كيفية تنفيذ المصادقة في Next.js
فهم المصادقة أمر بالغ الأهمية لحماية بيانات تطبيقك. سيرشدك هذه الصفحة إلى ميزات React وNext.js التي يمكن استخدامها لتنفيذ المصادقة.
قبل البدء، من المفيد تقسيم العملية إلى ثلاثة مفاهيم:
- المصادقة: التحقق مما إذا كان المستخدم هو من يدعي أنه هو. يتطلب من المستخدم إثبات هويته بشيء يمتلكه، مثل اسم المستخدم وكلمة المرور.
- إدارة الجلسة: تتبع حالة مصادقة المستخدم عبر الطلبات.
- الترخيص: تحديد المسارات والبيانات التي يمكن للمستخدم الوصول إليها.
يظهر هذا الرسم التخطيطي تدفق المصادقة باستخدام ميزات React وNext.js:

تقدم الأمثلة في هذه الصفحة شرحًا أساسيًا لمصادقة اسم المستخدم وكلمة المرور لأغراض تعليمية. بينما يمكنك تنفيذ حل مصادقة مخصص، نوصي باستخدام مكتبة مصادقة لزيادة الأمان والبساطة. توفر هذه المكتبات حلولًا مدمجة للمصادقة وإدارة الجلسات والترخيص، بالإضافة إلى ميزات إضافية مثل تسجيلات الدخول الاجتماعية والمصادقة متعددة العوامل والتحكم في الوصول القائم على الأدوار. يمكنك العثور على قائمة في قسم مكتبات المصادقة.
المصادقة
وظائف التسجيل وتسجيل الدخول
يمكنك استخدام عنصر <form>
مع إجراءات الخادم في React وuseActionState
لالتقاط بيانات اعتماد المستخدم والتحقق من صحة حقول النموذج واستدعاء واجهة برمجة التطبيقات أو قاعدة البيانات الخاصة بمزود المصادقة.
نظرًا لأن إجراءات الخادم تنفذ دائمًا على الخادم، فإنها توفر بيئة آمنة للتعامل مع منطق المصادقة.
إليك الخطوات لتنفيذ وظائف التسجيل/تسجيل الدخول:
1. التقاط بيانات اعتماد المستخدم
للتقاط بيانات اعتماد المستخدم، أنشئ نموذجًا يستدعي إجراء خادم عند الإرسال. على سبيل المثال، نموذج تسجيل يقبل اسم المستخدم والبريد الإلكتروني وكلمة المرور:
import { signup } from '@/app/actions/auth'
export function SignupForm() {
return (
<form action={signup}>
<div>
<label htmlFor="name">Name</label>
<input id="name" name="name" placeholder="Name" />
</div>
<div>
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" placeholder="Email" />
</div>
<div>
<label htmlFor="password">Password</label>
<input id="password" name="password" type="password" />
</div>
<button type="submit">Sign Up</button>
</form>
)
}
import { signup } from '@/app/actions/auth'
export function SignupForm() {
return (
<form action={signup}>
<div>
<label htmlFor="name">Name</label>
<input id="name" name="name" placeholder="Name" />
</div>
<div>
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" placeholder="Email" />
</div>
<div>
<label htmlFor="password">Password</label>
<input id="password" name="password" type="password" />
</div>
<button type="submit">Sign Up</button>
</form>
)
}
export async function signup(formData: FormData) {}
export async function signup(formData) {}
2. التحقق من صحة حقول النموذج على الخادم
استخدم إجراء الخادم للتحقق من صحة حقول النموذج على الخادم. إذا كان مزود المصادقة لا يوفر التحقق من صحة النموذج، يمكنك استخدام مكتبة تحقق من المخطط مثل Zod أو Yup.
باستخدام Zod كمثال، يمكنك تحديد مخطط نموذج مع رسائل خطأ مناسبة:
import { z } from 'zod'
export const SignupFormSchema = z.object({
name: z
.string()
.min(2, { message: 'Name must be at least 2 characters long.' })
.trim(),
email: z.string().email({ message: 'Please enter a valid email.' }).trim(),
password: z
.string()
.min(8, { message: 'Be at least 8 characters long' })
.regex(/[a-zA-Z]/, { message: 'Contain at least one letter.' })
.regex(/[0-9]/, { message: 'Contain at least one number.' })
.regex(/[^a-zA-Z0-9]/, {
message: 'Contain at least one special character.',
})
.trim(),
})
export type FormState =
| {
errors?: {
name?: string[]
email?: string[]
password?: string[]
}
message?: string
}
| undefined
import { z } from 'zod'
export const SignupFormSchema = z.object({
name: z
.string()
.min(2, { message: 'Name must be at least 2 characters long.' })
.trim(),
email: z.string().email({ message: 'Please enter a valid email.' }).trim(),
password: z
.string()
.min(8, { message: 'Be at least 8 characters long' })
.regex(/[a-zA-Z]/, { message: 'Contain at least one letter.' })
.regex(/[0-9]/, { message: 'Contain at least one number.' })
.regex(/[^a-zA-Z0-9]/, {
message: 'Contain at least one special character.',
})
.trim(),
})
لمنع استدعاءات غير ضرورية لواجهة برمجة التطبيقات أو قاعدة البيانات الخاصة بمزود المصادقة، يمكنك إرجاع
مبكرًا في إجراء الخادم إذا كانت أي حقول نموذج لا تتطابق مع المخطط المحدد.
import { SignupFormSchema, FormState } from '@/app/lib/definitions'
export async function signup(state: FormState, formData: FormData) {
// Validate form fields
const validatedFields = SignupFormSchema.safeParse({
name: formData.get('name'),
email: formData.get('email'),
password: formData.get('password'),
})
// If any form fields are invalid, return early
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
}
}
// Call the provider or db to create a user...
}
import { SignupFormSchema } from '@/app/lib/definitions'
export async function signup(state, formData) {
// Validate form fields
const validatedFields = SignupFormSchema.safeParse({
name: formData.get('name'),
email: formData.get('email'),
password: formData.get('password'),
})
// If any form fields are invalid, return early
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
}
}
// Call the provider or db to create a user...
}
في نموذج <SignupForm />
الخاص بك، يمكنك استخدام خطاف useActionState
في React لعرض أخطاء التحقق أثناء إرسال النموذج:
'use client'
import { signup } from '@/app/actions/auth'
import { useActionState } from 'react'
export default function SignupForm() {
const [state, action, pending] = useActionState(signup, undefined)
return (
<form action={action}>
<div>
<label htmlFor="name">Name</label>
<input id="name" name="name" placeholder="Name" />
</div>
{state?.errors?.name && <p>{state.errors.name}</p>}
<div>
<label htmlFor="email">Email</label>
<input id="email" name="email" placeholder="Email" />
</div>
{state?.errors?.email && <p>{state.errors.email}</p>}
<div>
<label htmlFor="password">Password</label>
<input id="password" name="password" type="password" />
</div>
{state?.errors?.password && (
<div>
<p>Password must:</p>
<ul>
{state.errors.password.map((error) => (
<li key={error}>- {error}</li>
))}
</ul>
</div>
)}
<button disabled={pending} type="submit">
Sign Up
</button>
</form>
)
}
'use client'
import { signup } from '@/app/actions/auth'
import { useActionState } from 'react'
export default function SignupForm() {
const [state, action, pending] = useActionState(signup, undefined)
return (
<form action={action}>
<div>
<label htmlFor="name">Name</label>
<input id="name" name="name" placeholder="Name" />
</div>
{state?.errors?.name && <p>{state.errors.name}</p>}
<div>
<label htmlFor="email">Email</label>
<input id="email" name="email" placeholder="Email" />
</div>
{state?.errors?.email && <p>{state.errors.email}</p>}
<div>
<label htmlFor="password">Password</label>
<input id="password" name="password" type="password" />
</div>
{state?.errors?.password && (
<div>
<p>Password must:</p>
<ul>
{state.errors.password.map((error) => (
<li key={error}>- {error}</li>
))}
</ul>
</div>
)}
<button disabled={pending} type="submit">
Sign Up
</button>
</form>
)
}
جيد أن تعرف:
- في React 19، يتضمن
useFormStatus
مفاتيح إضافية على الكائن المعاد، مثل data وmethod وaction. إذا كنت لا تستخدم React 19، فإن المفتاحpending
فقط متاح.- قبل تعديل البيانات، يجب عليك دائمًا التأكد من أن المستخدم مصرح له أيضًا بتنفيذ الإجراء. راجع المصادقة والترخيص.
٣. إنشاء مستخدم أو التحقق من بيانات الاعتماد
بعد التحقق من حقول النموذج، يمكنك إنشاء حساب مستخدم جديد أو التحقق من وجود المستخدم عن طريق استدعاء واجهة برمجة التطبيقات (API) أو قاعدة البيانات الخاصة بمزود المصادقة.
متابعة من المثال السابق:
export async function signup(state: FormState, formData: FormData) {
// 1. التحقق من حقول النموذج
// ...
// 2. تحضير البيانات للإدراج في قاعدة البيانات
const { name, email, password } = validatedFields.data
// مثلاً: تشفير كلمة مرور المستخدم قبل تخزينها
const hashedPassword = await bcrypt.hash(password, 10)
// 3. إدراج المستخدم في قاعدة البيانات أو استدعاء واجهة برمجة التطبيقات لمكتبة المصادقة
const data = await db
.insert(users)
.values({
name,
email,
password: hashedPassword,
})
.returning({ id: users.id })
const user = data[0]
if (!user) {
return {
message: 'حدث خطأ أثناء إنشاء حسابك.',
}
}
// TODO:
// 4. إنشاء جلسة المستخدم
// 5. توجيه المستخدم
}
export async function signup(state, formData) {
// 1. التحقق من حقول النموذج
// ...
// 2. تحضير البيانات للإدراج في قاعدة البيانات
const { name, email, password } = validatedFields.data
// مثلاً: تشفير كلمة مرور المستخدم قبل تخزينها
const hashedPassword = await bcrypt.hash(password, 10)
// 3. إدراج المستخدم في قاعدة البيانات أو استدعاء واجهة برمجة التطبيقات لمكتبة المصادقة
const data = await db
.insert(users)
.values({
name,
email,
password: hashedPassword,
})
.returning({ id: users.id })
const user = data[0]
if (!user) {
return {
message: 'حدث خطأ أثناء إنشاء حسابك.',
}
}
// TODO:
// 4. إنشاء جلسة المستخدم
// 5. توجيه المستخدم
}
بعد إنشاء حساب المستخدم بنجاح أو التحقق من بيانات الاعتماد، يمكنك إنشاء جلسة لإدارة حالة مصادقة المستخدم. اعتمادًا على استراتيجية إدارة الجلسة، يمكن تخزين الجلسة في ملف تعريف الارتباط (cookie) أو قاعدة البيانات، أو كليهما. انتقل إلى قسم إدارة الجلسة لمعرفة المزيد.
نصائح:
- المثال أعلاه مفصل لأنه يفصل خطوات المصادقة لأغراض تعليمية. هذا يسلط الضوء على أن تنفيذ حل آمن خاص بك يمكن أن يصبح معقدًا بسرعة. فكر في استخدام مكتبة مصادقة لتبسيط العملية.
- لتحسين تجربة المستخدم، قد ترغب في التحقق من تكرار البريد الإلكتروني أو اسم المستخدم في وقت مبكر من عملية التسجيل. على سبيل المثال، أثناء كتابة المستخدم لاسم المستخدم أو عند فقدان حقل الإدخال للتركيز. يمكن أن يساعد ذلك في منع عمليات إرسال النماذج غير الضرورية وتقديم ملاحظات فورية للمستخدم. يمكنك التحكم في تكرار هذه الفحوصات باستخدام مكتبات مثل use-debounce.
إدارة الجلسة
تضمن إدارة الجلسة الحفاظ على حالة مصادقة المستخدم عبر الطلبات. وهي تشمل إنشاء وتخزين وتحديث وحذف الجلسات أو الرموز (tokens).
هناك نوعان من الجلسات:
١. بدون حالة (Stateless): يتم تخزين بيانات الجلسة (أو الرمز) في ملفات تعريف الارتباط (cookies) للمتصفح. يتم إرسال ملف تعريف الارتباط مع كل طلب، مما يسمح بالتحقق من الجلسة على الخادم. هذه الطريقة أبسط، ولكن يمكن أن تكون أقل أمانًا إذا لم يتم تنفيذها بشكل صحيح. ٢. قاعدة البيانات (Database): يتم تخزين بيانات الجلسة في قاعدة بيانات، مع تلقي متصفح المستخدم فقط لمعرف الجلسة المشفر. هذه الطريقة أكثر أمانًا، ولكن يمكن أن تكون معقدة وتستخدم موارد أكثر على الخادم.
من الجيد معرفة: بينما يمكنك استخدام أي من الطريقتين، أو كليهما، نوصي باستخدام مكتبة إدارة جلسات مثل iron-session أو Jose.
الجلسات بدون حالة (Stateless)
لإنشاء وإدارة الجلسات بدون حالة، هناك بعض الخطوات التي تحتاج إلى اتباعها:
١. إنشاء مفتاح سري، والذي سيتم استخدامه لتوقيع جلسة العمل الخاصة بك، وتخزينه كـ متغير بيئي.
٢. كتابة منطق لتشفير/فك تشفير بيانات الجلسة باستخدام مكتبة إدارة الجلسات.
٣. إدارة ملفات تعريف الارتباط باستخدام واجهة برمجة التطبيقات cookies
الخاصة بـ Next.js.
بالإضافة إلى ما سبق، ضع في اعتبارك إضافة وظيفة لتحديث (أو تجديد) الجلسة عندما يعود المستخدم إلى التطبيق، وحذف الجلسة عندما يقوم المستخدم بتسجيل الخروج.
من الجيد معرفة: تحقق مما إذا كانت مكتبة المصادقة الخاصة بك تتضمن إدارة الجلسات.
١. إنشاء مفتاح سري
هناك بعض الطرق التي يمكنك من خلالها إنشاء مفتاح سري لتوقيع جلسة العمل الخاصة بك. على سبيل المثال، يمكنك اختيار استخدام أمر openssl
في طرفيتك:
openssl rand -base64 32
ينشئ هذا الأمر سلسلة عشوائية مكونة من 32 حرفًا يمكنك استخدامها كمفتاح سري وتخزينها في ملف المتغيرات البيئية:
SESSION_SECRET=your_secret_key
يمكنك بعد ذلك الرجوع إلى هذا المفتاح في منطق إدارة الجلسة:
const secretKey = process.env.SESSION_SECRET
٢. تشفير وفك تشفير الجلسات
بعد ذلك، يمكنك استخدام مكتبة إدارة الجلسات المفضلة لديك لتشفير وفك تشفير الجلسات. متابعة من المثال السابق، سنستخدم Jose (متوافق مع Edge Runtime) وحزمة server-only
الخاصة بـ React لضمان تنفيذ منطق إدارة الجلسة فقط على الخادم.
import 'server-only'
import { SignJWT, jwtVerify } from 'jose'
import { SessionPayload } from '@/app/lib/definitions'
const secretKey = process.env.SESSION_SECRET
const encodedKey = new TextEncoder().encode(secretKey)
export async function encrypt(payload: SessionPayload) {
return new SignJWT(payload)
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('7d')
.sign(encodedKey)
}
export async function decrypt(session: string | undefined = '') {
try {
const { payload } = await jwtVerify(session, encodedKey, {
algorithms: ['HS256'],
})
return payload
} catch (error) {
console.log('فشل التحقق من الجلسة')
}
}
import 'server-only'
import { SignJWT, jwtVerify } from 'jose'
const secretKey = process.env.SESSION_SECRET
const encodedKey = new TextEncoder().encode(secretKey)
export async function encrypt(payload) {
return new SignJWT(payload)
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('7d')
.sign(encodedKey)
}
export async function decrypt(session) {
try {
const { payload } = await jwtVerify(session, encodedKey, {
algorithms: ['HS256'],
})
return payload
} catch (error) {
console.log('فشل التحقق من الجلسة')
}
}
نصائح:
- يجب أن يحتوي الحمولة (payload) على الحد الأدنى من بيانات المستخدم الفريدة التي سيتم استخدامها في الطلبات اللاحقة، مثل معرف المستخدم، الدور، إلخ. لا ينبغي أن تحتوي على معلومات تعريف شخصية مثل رقم الهاتف، عنوان البريد الإلكتروني، معلومات بطاقة الائتمان، إلخ، أو بيانات حساسة مثل كلمات المرور.
٣. تعيين ملفات تعريف الارتباط (خيارات موصى بها)
لتخزين الجلسة في ملف تعريف ارتباط، استخدم واجهة برمجة التطبيقات cookies
الخاصة بـ Next.js. يجب تعيين ملف تعريف الارتباط على الخادم، ويتضمن الخيارات الموصى بها:
- HttpOnly: يمنع JavaScript من الوصول إلى ملف تعريف الارتباط من جانب العميل.
- Secure: استخدم https لإرسال ملف تعريف الارتباط.
- SameSite: حدد ما إذا كان يمكن إرسال ملف تعريف الارتباط مع طلبات cross-site.
- Max-Age أو Expires: احذف ملف تعريف الارتباط بعد فترة معينة.
- Path: حدد مسار URL لملف تعريف الارتباط.
يرجى الرجوع إلى MDN لمزيد من المعلومات حول كل من هذه الخيارات.
import 'server-only'
import { cookies } from 'next/headers'
export async function createSession(userId: string) {
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
const session = await encrypt({ userId, expiresAt })
const cookieStore = await cookies()
cookieStore.set('session', session, {
httpOnly: true,
secure: true,
expires: expiresAt,
sameSite: 'lax',
path: '/',
})
}
import 'server-only'
import { cookies } from 'next/headers'
export async function createSession(userId) {
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
const session = await encrypt({ userId, expiresAt })
const cookieStore = await cookies()
cookieStore.set('session', session, {
httpOnly: true,
secure: true,
expires: expiresAt,
sameSite: 'lax',
path: '/',
})
}
بالعودة إلى إجراء الخادم الخاص بك، يمكنك استدعاء الدالة createSession()
، واستخدام واجهة برمجة التطبيقات redirect()
لتوجيه المستخدم إلى الصفحة المناسبة:
import { createSession } from '@/app/lib/session'
export async function signup(state: FormState, formData: FormData) {
// الخطوات السابقة:
// 1. التحقق من حقول النموذج
// 2. تحضير البيانات للإدراج في قاعدة البيانات
// 3. إدراج المستخدم في قاعدة البيانات أو استدعاء واجهة برمجة التطبيقات لمكتبة المصادقة
// الخطوات الحالية:
// 4. إنشاء جلسة المستخدم
await createSession(user.id)
// 5. توجيه المستخدم
redirect('/profile')
}
import { createSession } from '@/app/lib/session'
export async function signup(state, formData) {
// الخطوات السابقة:
// 1. التحقق من حقول النموذج
// 2. تحضير البيانات للإدراج في قاعدة البيانات
// 3. إدراج المستخدم في قاعدة البيانات أو استدعاء واجهة برمجة التطبيقات لمكتبة المصادقة
// الخطوات الحالية:
// 4. إنشاء جلسة المستخدم
await createSession(user.id)
// 5. توجيه المستخدم
redirect('/profile')
}
نصائح:
- يجب تعيين ملفات تعريف الارتباط على الخادم لمنع العبث من جانب العميل.
- 🎥 شاهد: تعرف على المزيد حول الجلسات بدون حالة والمصادقة مع Next.js → YouTube (11 دقيقة).
تحديث (أو تجديد) الجلسات
يمكنك أيضًا تمديد وقت انتهاء صلاحية الجلسة. هذا مفيد لإبقاء المستخدم مسجل الدخول بعد عودته إلى التطبيق. على سبيل المثال:
import 'server-only'
import { cookies } from 'next/headers'
import { decrypt } from '@/app/lib/session'
export async function updateSession() {
const session = (await cookies()).get('session')?.value
const payload = await decrypt(session)
if (!session || !payload) {
return null
}
const expires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
const cookieStore = await cookies()
cookieStore.set('session', session, {
httpOnly: true,
secure: true,
expires: expires,
sameSite: 'lax',
path: '/',
})
}
import 'server-only'
import { cookies } from 'next/headers'
import { decrypt } from '@/app/lib/session'
export async function updateSession() {
const session = (await cookies()).get('session')?.value
const payload = await decrypt(session)
if (!session || !payload) {
return null
}
const expires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)(
await cookies()
).set('session', session, {
httpOnly: true,
secure: true,
expires: expires,
sameSite: 'lax',
path: '/',
})
}
نصيحة: تحقق مما إذا كانت مكتبة المصادقة الخاصة بك تدعم رموز التحديث (refresh tokens)، والتي يمكن استخدامها لتمديد جلسة المستخدم.
حذف الجلسة
لحذف الجلسة، يمكنك حذف ملف تعريف الارتباط (cookie):
import 'server-only'
import { cookies } from 'next/headers'
export async function deleteSession() {
const cookieStore = await cookies()
cookieStore.delete('session')
}
import 'server-only'
import { cookies } from 'next/headers'
export async function deleteSession() {
const cookieStore = await cookies()
cookieStore.delete('session')
}
ثم يمكنك إعادة استخدام دالة deleteSession()
في تطبيقك، على سبيل المثال عند تسجيل الخروج:
import { cookies } from 'next/headers'
import { deleteSession } from '@/app/lib/session'
export async function logout() {
await deleteSession()
redirect('/login')
}
import { cookies } from 'next/headers'
import { deleteSession } from '@/app/lib/session'
export async function logout() {
await deleteSession()
redirect('/login')
}
جلسات قاعدة البيانات
لإنشاء وإدارة جلسات قاعدة البيانات، ستحتاج إلى اتباع هذه الخطوات:
- إنشاء جدول في قاعدة البيانات لتخزين بيانات الجلسة (أو التحقق مما إذا كانت مكتبة المصادقة الخاصة بك تتعامل مع هذا).
- تنفيذ وظائف لإدراج وتحديث وحذف الجلسات
- تشفير معرّف الجلسة قبل تخزينه في متصفح المستخدم، والتأكد من تزامن قاعدة البيانات وملف تعريف الارتباط (هذا اختياري، ولكنه موصى به للتحقق المتفائل من المصادقة في الوسيط).
على سبيل المثال:
import cookies from 'next/headers'
import { db } from '@/app/lib/db'
import { encrypt } from '@/app/lib/session'
export async function createSession(id: number) {
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
// 1. إنشاء جلسة في قاعدة البيانات
const data = await db
.insert(sessions)
.values({
userId: id,
expiresAt,
})
// إرجاع معرّف الجلسة
.returning({ id: sessions.id })
const sessionId = data[0].id
// 2. تشفير معرّف الجلسة
const session = await encrypt({ sessionId, expiresAt })
// 3. تخزين الجلسة في ملفات تعريف الارتباط للتحقق المتفائل من المصادقة
const cookieStore = await cookies()
cookieStore.set('session', session, {
httpOnly: true,
secure: true,
expires: expiresAt,
sameSite: 'lax',
path: '/',
})
}
import cookies from 'next/headers'
import { db } from '@/app/lib/db'
import { encrypt } from '@/app/lib/session'
export async function createSession(id) {
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
// 1. إنشاء جلسة في قاعدة البيانات
const data = await db
.insert(sessions)
.values({
userId: id,
expiresAt,
})
// إرجاع معرّف الجلسة
.returning({ id: sessions.id })
const sessionId = data[0].id
// 2. تشفير معرّف الجلسة
const session = await encrypt({ sessionId, expiresAt })
// 3. تخزين الجلسة في ملفات تعريف الارتباط للتحقق المتفائل من المصادقة
const cookieStore = await cookies()
cookieStore.set('session', session, {
httpOnly: true,
secure: true,
expires: expiresAt,
sameSite: 'lax',
path: '/',
})
}
نصائح:
- للوصول الأسرع، يمكنك التفكير في إضافة تخزين مؤقت على الخادم لمدة الجلسة. يمكنك أيضًا الاحتفاظ ببيانات الجلسة في قاعدة البيانات الرئيسية، وجمع طلبات البيانات لتقليل عدد الاستعلامات.
- يمكنك اختيار استخدام جلسات قاعدة البيانات لحالات استخدام أكثر تقدمًا، مثل تتبع آخر مرة قام فيها المستخدم بتسجيل الدخول، أو عدد الأجهزة النشطة، أو منح المستخدمين القدرة على تسجيل الخروج من جميع الأجهزة.
بعد تنفيذ إدارة الجلسات، ستحتاج إلى إضافة منطق التفويض للتحكم فيما يمكن للمستخدمين الوصول إليه والقيام به داخل تطبيقك. تابع إلى قسم التفويض لمعرفة المزيد.
التفويض
بعد مصادقة المستخدم وإنشاء جلسة، يمكنك تنفيذ التفويض للتحكم فيما يمكن للمستخدم الوصول إليه والقيام به داخل تطبيقك.
هناك نوعان رئيسيان من عمليات التحقق من التفويض:
- المتفائل: يتحقق مما إذا كان المستخدم مخولاً للوصول إلى مسار أو تنفيذ إجراء باستخدام بيانات الجلسة المخزنة في ملف تعريف الارتباط. هذه الفحوصات مفيدة للعمليات السريعة، مثل إظهار/إخفاء عناصر واجهة المستخدم أو إعادة توجيه المستخدمين بناءً على الأذونات أو الأدوار.
- الآمن: يتحقق مما إذا كان المستخدم مخولاً للوصول إلى مسار أو تنفيذ إجراء باستخدام بيانات الجلسة المخزنة في قاعدة البيانات. هذه الفحوصات أكثر أمانًا وتستخدم للعمليات التي تتطلب الوصول إلى بيانات حساسة أو إجراءات.
لكلا الحالتين، نوصي بما يلي:
- إنشاء طبقة وصول البيانات لمركزية منطق التفويض
- استخدام كائنات نقل البيانات (DTO) لإرجاع البيانات الضرورية فقط
- استخدام الوسيط اختياريًا لإجراء فحوصات متفائلة.
فحوصات متفائلة باستخدام الوسيط (اختياري)
هناك بعض الحالات التي قد ترغب في استخدام الوسيط وإعادة توجيه المستخدمين بناءً على الأذونات:
- لإجراء فحوصات متفائلة. نظرًا لأن الوسيط يعمل على كل مسار، فهو طريقة جيدة لمركزية منطق إعادة التوجيه وتصفية المستخدمين غير المصرح لهم مسبقًا.
- لحماية المسارات الثابتة التي تشارك البيانات بين المستخدمين (مثل المحتوى خلف جدار الدفع).
ومع ذلك، نظرًا لأن الوسيط يعمل على كل مسار، بما في ذلك المسارات المحملة مسبقًا، فمن المهم قراءة الجلسة فقط من ملف تعريف الارتباط (فحوصات متفائلة)، وتجنب فحوصات قاعدة البيانات لمنع مشكلات الأداء.
على سبيل المثال:
import { NextRequest, NextResponse } from 'next/server'
import { decrypt } from '@/app/lib/session'
import { cookies } from 'next/headers'
// 1. تحديد المسارات المحمية والعامة
const protectedRoutes = ['/dashboard']
const publicRoutes = ['/login', '/signup', '/']
export default async function middleware(req: NextRequest) {
// 2. التحقق مما إذا كان المسار الحالي محميًا أو عامًا
const path = req.nextUrl.pathname
const isProtectedRoute = protectedRoutes.includes(path)
const isPublicRoute = publicRoutes.includes(path)
// 3. فك تشفير الجلسة من ملف تعريف الارتباط
const cookie = (await cookies()).get('session')?.value
const session = await decrypt(cookie)
// 4. إعادة التوجيه إلى /login إذا لم يكن المستخدم مصادقًا عليه
if (isProtectedRoute && !session?.userId) {
return NextResponse.redirect(new URL('/login', req.nextUrl))
}
// 5. إعادة التوجيه إلى /dashboard إذا كان المستخدم مصادقًا عليه
if (
isPublicRoute &&
session?.userId &&
!req.nextUrl.pathname.startsWith('/dashboard')
) {
return NextResponse.redirect(new URL('/dashboard', req.nextUrl))
}
return NextResponse.next()
}
// المسارات التي لا يجب أن يعمل عليها الوسيط
export const config = {
matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
}
import { NextResponse } from 'next/server'
import { decrypt } from '@/app/lib/session'
import { cookies } from 'next/headers'
// 1. تحديد المسارات المحمية والعامة
const protectedRoutes = ['/dashboard']
const publicRoutes = ['/login', '/signup', '/']
export default async function middleware(req) {
// 2. التحقق مما إذا كان المسار الحالي محميًا أو عامًا
const path = req.nextUrl.pathname
const isProtectedRoute = protectedRoutes.includes(path)
const isPublicRoute = publicRoutes.includes(path)
// 3. فك تشفير الجلسة من ملف تعريف الارتباط
const cookie = (await cookies()).get('session')?.value
const session = await decrypt(cookie)
// 5. إعادة التوجيه إلى /login إذا لم يكن المستخدم مصادقًا عليه
if (isProtectedRoute && !session?.userId) {
return NextResponse.redirect(new URL('/login', req.nextUrl))
}
// 6. إعادة التوجيه إلى /dashboard إذا كان المستخدم مصادقًا عليه
if (
isPublicRoute &&
session?.userId &&
!req.nextUrl.pathname.startsWith('/dashboard')
) {
return NextResponse.redirect(new URL('/dashboard', req.nextUrl))
}
return NextResponse.next()
}
// المسارات التي لا يجب أن يعمل عليها الوسيط
export const config = {
matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
}
بينما يمكن أن يكون الوسيط مفيدًا للفحوصات الأولية، لا يجب أن يكون خط دفاعك الوحيد في حماية بياناتك. يجب إجراء غالبية فحوصات الأمان بالقرب من مصدر البيانات قدر الإمكان، راجع طبقة وصول البيانات لمزيد من المعلومات.
نصائح:
- في الوسيط، يمكنك أيضًا قراءة ملفات تعريف الارتباط باستخدام
req.cookies.get('session').value
.- يستخدم الوسيط وقت تشغيل الحافة، تحقق مما إذا كانت مكتبة المصادقة ومكتبة إدارة الجلسات متوافقة.
- يمكنك استخدام خاصية
matcher
في الوسيط لتحديد المسارات التي يجب أن يعمل عليها الوسيط. ومع ذلك، للمصادقة، يوصى بأن يعمل الوسيط على جميع المسارات.
إنشاء طبقة وصول البيانات (DAL)
نوصي بإنشاء DAL لمركزية طلبات البيانات ومنطق التفويض.
يجب أن تتضمن DAL دالة تتحقق من جلسة المستخدم أثناء تفاعله مع تطبيقك. على الأقل، يجب أن تتحقق الدالة مما إذا كانت الجلسة صالحة، ثم تقوم بإعادة التوجيه أو إرجاع معلومات المستخدم اللازمة لإجراء طلبات إضافية.
على سبيل المثال، قم بإنشاء ملف منفصل لـ DAL يتضمن دالة verifySession()
. ثم استخدم واجهة برمجة التطبيقات cache من React لحفظ قيمة الإرجاع للدالة أثناء تمرير عرض React:
import 'server-only'
import { cookies } from 'next/headers'
import { decrypt } from '@/app/lib/session'
export const verifySession = cache(async () => {
const cookie = (await cookies()).get('session')?.value
const session = await decrypt(cookie)
if (!session?.userId) {
redirect('/login')
}
return { isAuth: true, userId: session.userId }
})
import 'server-only'
import { cookies } from 'next/headers'
import { decrypt } from '@/app/lib/session'
export const verifySession = cache(async () => {
const cookie = (await cookies()).get('session')?.value
const session = await decrypt(cookie)
if (!session.userId) {
redirect('/login')
}
return { isAuth: true, userId: session.userId }
})
يمكنك بعد ذلك استدعاء دالة verifySession()
في طلبات البيانات، وإجراءات الخادم، ومعالجات المسار:
export const getUser = cache(async () => {
const session = await verifySession()
if (!session) return null
try {
const data = await db.query.users.findMany({
where: eq(users.id, session.userId),
// إرجاع الأعمدة التي تحتاجها صراحة بدلاً من كائن المستخدم بالكامل
columns: {
id: true,
name: true,
email: true,
},
})
const user = data[0]
return user
} catch (error) {
console.log('فشل في جلب المستخدم')
return null
}
})
export const getUser = cache(async () => {
const session = await verifySession()
if (!session) return null
try {
const data = await db.query.users.findMany({
where: eq(users.id, session.userId),
// إرجاع الأعمدة التي تحتاجها صراحة بدلاً من كائن المستخدم بالكامل
columns: {
id: true,
name: true,
email: true,
},
})
const user = data[0]
return user
} catch (error) {
console.log('فشل في جلب المستخدم')
return null
}
})
نصيحة:
- يمكن استخدام DAL لحماية البيانات التي يتم جلبها في وقت الطلب. ومع ذلك، بالنسبة للمسارات الثابتة التي تشارك البيانات بين المستخدمين، سيتم جلب البيانات في وقت البناء وليس في وقت الطلب. استخدم الوسيط لحماية المسارات الثابتة.
- للفحوصات الآمنة، يمكنك التحقق مما إذا كانت الجلسة صالحة عن طريق مقارنة معرّف الجلسة مع قاعدة البيانات الخاصة بك. استخدم دالة cache من React لتجنب الطلبات المكررة غير الضرورية إلى قاعدة البيانات أثناء تمرير العرض.
- قد ترغب في توحيد طلبات البيانات ذات الصلة في فئة JavaScript تقوم بتشغيل
verifySession()
قبل أي طرق.
استخدام كائنات نقل البيانات (DTO)
عند استرجاع البيانات، يُنصح بإرجاع البيانات الضرورية فقط التي سيتم استخدامها في تطبيقك، وليس الكائنات الكاملة. على سبيل المثال، إذا كنت تسترجع بيانات المستخدم، فقد ترجع فقط معرف المستخدم واسمه، بدلاً من كائن المستخدم الكامل الذي قد يحتوي على كلمات المرور وأرقام الهواتف، إلخ.
ومع ذلك، إذا لم يكن لديك تحكم في بنية البيانات المُرجعة، أو كنت تعمل ضمن فريق حيث تريد تجنب تمرير كائنات كاملة إلى العميل، يمكنك استخدام استراتيجيات مثل تحديد الحقول الآمنة للعرض على العميل.
import 'server-only'
import { getUser } from '@/app/lib/dal'
function canSeeUsername(viewer: User) {
return true
}
function canSeePhoneNumber(viewer: User, team: string) {
return viewer.isAdmin || team === viewer.team
}
export async function getProfileDTO(slug: string) {
const data = await db.query.users.findMany({
where: eq(users.slug, slug),
// Return specific columns here
})
const user = data[0]
const currentUser = await getUser(user.id)
// Or return only what's specific to the query here
return {
username: canSeeUsername(currentUser) ? user.username : null,
phonenumber: canSeePhoneNumber(currentUser, user.team)
? user.phonenumber
: null,
}
}
import 'server-only'
import { getUser } from '@/app/lib/dal'
function canSeeUsername(viewer) {
return true
}
function canSeePhoneNumber(viewer, team) {
return viewer.isAdmin || team === viewer.team
}
export async function getProfileDTO(slug) {
const data = await db.query.users.findMany({
where: eq(users.slug, slug),
// Return specific columns here
})
const user = data[0]
const currentUser = await getUser(user.id)
// Or return only what's specific to the query here
return {
username: canSeeUsername(currentUser) ? user.username : null,
phonenumber: canSeePhoneNumber(currentUser, user.team)
? user.phonenumber
: null,
}
}
من خلال تركيز طلبات البيانات ومنطق التفويض في طبقة الوصول إلى البيانات (DAL) واستخدام كائنات نقل البيانات (DTOs)، يمكنك ضمان أن جميع طلبات البيانات آمنة ومتسقة، مما يسهل الصيانة والتدقيق والتصحيح مع توسع تطبيقك.
معلومة مفيدة:
- هناك عدة طرق مختلفة لتحديد كائن نقل البيانات (DTO)، بدءًا من استخدام
toJSON()
، إلى دوال فردية مثل المثال أعلاه، أو فئات جافاسكريبت. نظرًا لأن هذه أنماط جافاسكريبت وليست ميزة في React أو Next.js، ننصح بإجراء بعض البحث للعثور على النمط الأنسب لتطبيقك.- تعلم المزيد عن أفضل ممارسات الأمان في مقال الأمان في Next.js.
مكونات الخادم (Server Components)
فحص التفويض في مكونات الخادم مفيد للوصول القائم على الأدوار. على سبيل المثال، لعرض المكونات بشكل مشروط بناءً على دور المستخدم:
import { verifySession } from '@/app/lib/dal'
export default function Dashboard() {
const session = await verifySession()
const userRole = session?.user?.role // Assuming 'role' is part of the session object
if (userRole === 'admin') {
return <AdminDashboard />
} else if (userRole === 'user') {
return <UserDashboard />
} else {
redirect('/login')
}
}
import { verifySession } from '@/app/lib/dal'
export default function Dashboard() {
const session = await verifySession()
const userRole = session.role // Assuming 'role' is part of the session object
if (userRole === 'admin') {
return <AdminDashboard />
} else if (userRole === 'user') {
return <UserDashboard />
} else {
redirect('/login')
}
}
في المثال، نستخدم الدالة verifySession()
من طبقة الوصول إلى البيانات (DAL) للتحقق من أدوار 'admin' و'user' والأدوار غير المصرح بها. يضمن هذا النمط تفاعل كل مستخدم فقط مع المكونات المناسبة لدوره.
التخطيطات وفحوصات التفويض
بسبب التصيير الجزئي (Partial Rendering)، كن حذرًا عند إجراء الفحوصات في التخطيطات (Layouts) لأنها لا تُعيد التصيير أثناء التنقل، مما يعني أن جلسة المستخدم لن يتم التحقق منها في كل تغيير للمسار.
بدلاً من ذلك، يجب إجراء الفحوصات بالقرب من مصدر البيانات أو المكون الذي سيتم عرضه بشكل مشروط.
على سبيل المثال، ضع في الاعتبار تخطيطًا مشتركًا يسترجع بيانات المستخدم ويعرض صورة المستخدم في شريط التنقل. بدلاً من إجراء فحص التفويض في التخطيط، يجب استرجاع بيانات المستخدم (getUser()
) في التخطيط وإجراء فحص التفويض في طبقة الوصول إلى البيانات (DAL).
هذا يضمن أنه في أي مكان يتم فيه استدعاء getUser()
داخل تطبيقك، يتم إجراء فحص التفويض، ويمنع المطورين من نسيان التحقق من أن المستخدم مصرح له بالوصول إلى البيانات.
export default async function Layout({
children,
}: {
children: React.ReactNode;
}) {
const user = await getUser();
return (
// ...
)
}
export default async function Layout({ children }) {
const user = await getUser();
return (
// ...
)
}
export const getUser = cache(async () => {
const session = await verifySession()
if (!session) return null
// Get user ID from session and fetch data
})
export const getUser = cache(async () => {
const session = await verifySession()
if (!session) return null
// Get user ID from session and fetch data
})
معلومة مفيدة:
- النمط الشائع في التطبيقات ذات الصفحة الواحدة (SPAs) هو إرجاع
null
في تخطيط أو مكون رئيسي إذا لم يكن المستخدم مصرحًا له. لا يُنصح بهذا النمط لأن تطبيقات Next.js لديها نقاط دخول متعددة، مما لن يمنع أجزاء المسار المتداخلة وإجراءات الخادم (Server Actions) من الوصول.
إجراءات الخادم (Server Actions)
عامل إجراءات الخادم بنفس اعتبارات الأمان مثل نقاط نهاية واجهة برمجة التطبيقات (API) الموجهة للجمهور، وتحقق مما إذا كان المستخدم مسموحًا له بتنفيذ التغيير.
في المثال أدناه، نتحقق من دور المستخدم قبل السماح بتنفيذ الإجراء:
'use server'
import { verifySession } from '@/app/lib/dal'
export async function serverAction(formData: FormData) {
const session = await verifySession()
const userRole = session?.user?.role
// Return early if user is not authorized to perform the action
if (userRole !== 'admin') {
return null
}
// Proceed with the action for authorized users
}
'use server'
import { verifySession } from '@/app/lib/dal'
export async function serverAction() {
const session = await verifySession()
const userRole = session.user.role
// Return early if user is not authorized to perform the action
if (userRole !== 'admin') {
return null
}
// Proceed with the action for authorized users
}
معالجات المسار (Route Handlers)
عامل معالجات المسار بنفس اعتبارات الأمان مثل نقاط نهاية واجهة برمجة التطبيقات (API) الموجهة للجمهور، وتحقق مما إذا كان المستخدم مسموحًا له بالوصول إلى معالج المسار.
على سبيل المثال:
import { verifySession } from '@/app/lib/dal'
export async function GET() {
// User authentication and role verification
const session = await verifySession()
// Check if the user is authenticated
if (!session) {
// User is not authenticated
return new Response(null, { status: 401 })
}
// Check if the user has the 'admin' role
if (session.user.role !== 'admin') {
// User is authenticated but does not have the right permissions
return new Response(null, { status: 403 })
}
// Continue for authorized users
}
import { verifySession } from '@/app/lib/dal'
export async function GET() {
// User authentication and role verification
const session = await verifySession()
// Check if the user is authenticated
if (!session) {
// User is not authenticated
return new Response(null, { status: 401 })
}
// Check if the user has the 'admin' role
if (session.user.role !== 'admin') {
// User is authenticated but does not have the right permissions
return new Response(null, { status: 403 })
}
// Continue for authorized users
}
يوضح المثال أعلاه معالج مسار بفحص أمان من مستويين. أولاً يتحقق من وجود جلسة نشطة، ثم يتحقق مما إذا كان المستخدم المسجل دخوله هو 'admin'.
موفرو السياق (Context Providers)
استخدام موفري السياق للتفويض يعمل بسبب التداخل (interleaving). ومع ذلك، فإن context
في React غير مدعوم في مكونات الخادم، مما يجعلها قابلة للتطبيق فقط على مكونات العميل.
هذا يعمل، ولكن أي مكونات خادم فرعية سيتم تصييرها على الخادم أولاً، ولن يكون لديها وصول إلى بيانات الجلسة من موفر السياق:
import { ContextProvider } from 'auth-lib'
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<ContextProvider>{children}</ContextProvider>
</body>
</html>
)
}
'use client';
import { useSession } from "auth-lib";
export default function Profile() {
const { userId } = useSession();
const { data } = useSWR(`/api/user/${userId}`, fetcher)
return (
// ...
);
}
'use client';
import { useSession } from "auth-lib";
export default function Profile() {
const { userId } = useSession();
const { data } = useSWR(`/api/user/${userId}`, fetcher)
return (
// ...
);
}
إذا كانت بيانات الجلسة مطلوبة في مكونات العميل (مثل جلب البيانات من جانب العميل)، استخدم واجهة برمجة التطبيقات taintUniqueValue
في React لمنع بيانات الجلسة الحساسة من التعرض للعميل.
موارد
الآن بعد أن تعلمت عن المصادقة في Next.js، إليك مكتبات وموارد متوافقة مع Next.js لمساعدتك في تنفيذ المصادقة الآمنة وإدارة الجلسات:
مكتبات المصادقة
مكتبات إدارة الجلسات
قراءة إضافية
لمواصلة التعلم عن المصادقة والأمان، تحقق من الموارد التالية: