المصادقة في Next.js
مقدمة إلى المصادقة
المصادقة هي ميزة حاسمة في تطبيقات الويب الحديثة. يوفر Next.js خيارات مرنة لتنفيذ المصادقة، من حلول JWT البسيطة إلى موفري خدمات شاملين مثل NextAuth.js (الآن Auth.js). يغطي هذا الدرس التنفيذ الكامل للمصادقة.
فهم استراتيجيات المصادقة
قبل تنفيذ المصادقة، افهم الاستراتيجيات المختلفة المتاحة:
- قائمة على الجلسة: يخزن الخادم بيانات الجلسة، يرسل معرف الجلسة إلى العميل عبر ملف تعريف الارتباط
- قائمة على الرمز (JWT): يخزن العميل رمزاً يحتوي على بيانات المستخدم، يرسله مع كل طلب
- OAuth/تسجيل الدخول الاجتماعي: تفويض المصادقة إلى موفري خدمات خارجيين (Google، GitHub، إلخ.)
- بدون كلمة مرور: روابط سحرية عبر البريد الإلكتروني أو رموز SMS بدلاً من كلمات المرور
تثبيت وتكوين Auth.js
Auth.js هو حل المصادقة الأكثر شيوعاً لـ Next.js، حيث يوفر دعماً مدمجاً للعديد من موفري الخدمات والاستراتيجيات.
# تثبيت Auth.js npm install next-auth@beta # تثبيت موفري خدمات إضافيين إذا لزم الأمر npm install @auth/prisma-adapter npm install bcryptjs npm install @types/bcryptjs -D
<!-- lib/auth.ts -->
import NextAuth from 'next-auth'
import Google from 'next-auth/providers/google'
import GitHub from 'next-auth/providers/github'
import Credentials from 'next-auth/providers/credentials'
import { PrismaAdapter } from '@auth/prisma-adapter'
import { prisma } from './prisma'
import bcrypt from 'bcryptjs'
export const { handlers, auth, signIn, signOut } = NextAuth({
adapter: PrismaAdapter(prisma),
session: {
strategy: 'jwt',
},
pages: {
signIn: '/login',
error: '/auth/error',
},
providers: [
Google({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
GitHub({
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
}),
Credentials({
name: 'credentials',
credentials: {
email: { label: 'Email', type: 'email' },
password: { label: 'Password', type: 'password' }
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) {
throw new Error('Invalid credentials')
}
const user = await prisma.user.findUnique({
where: {
email: credentials.email as string
}
})
if (!user || !user?.hashedPassword) {
throw new Error('Invalid credentials')
}
const isCorrectPassword = await bcrypt.compare(
credentials.password as string,
user.hashedPassword
)
if (!isCorrectPassword) {
throw new Error('Invalid credentials')
}
return user
}
})
],
callbacks: {
async jwt({ token, user }) {
if (user) {
token.id = user.id
token.role = user.role
}
return token
},
async session({ session, token }) {
if (token && session.user) {
session.user.id = token.id as string
session.user.role = token.role as string
}
return session
}
}
})
<!-- app/api/auth/[...nextauth]/route.ts -->
import { handlers } from '@/lib/auth'
export const { GET, POST } = handlers
تكوين البيئة
أنشئ ملف .env الخاص بك مع بيانات الاعتماد الضرورية للمصادقة:
# .env NEXTAUTH_URL=http://localhost:3000 NEXTAUTH_SECRET=your-super-secret-key-here # موفرو OAuth GOOGLE_CLIENT_ID=your-google-client-id GOOGLE_CLIENT_SECRET=your-google-client-secret GITHUB_CLIENT_ID=your-github-client-id GITHUB_CLIENT_SECRET=your-github-client-secret # قاعدة البيانات DATABASE_URL="postgresql://user:password@localhost:5432/mydb"
openssl rand -base64 32. لا تستخدم أبداً سلسلة بسيطة في الإنتاج.
إنشاء موفري المصادقة
لجعل المصادقة متاحة في جميع أنحاء تطبيقك، أنشئ موفر جلسة:
<!-- app/providers/session-provider.tsx -->
'use client'
import { SessionProvider } from 'next-auth/react'
export function AuthProvider({
children,
}: {
children: React.ReactNode
}) {
return <SessionProvider>{children}</SessionProvider>
}
<!-- app/layout.tsx -->
import { AuthProvider } from './providers/session-provider'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="ar">
<body>
<AuthProvider>
{children}
</AuthProvider>
</body>
</html>
)
}
بناء مكونات تسجيل الدخول
أنشئ صفحة تسجيل دخول شاملة مع خيارات مصادقة متعددة:
<!-- app/login/page.tsx -->
import { LoginForm } from './login-form'
import { SocialLogins } from './social-logins'
export default function LoginPage() {
return (
<div className="max-w-md mx-auto mt-8 p-6">
<h1 className="text-2xl font-bold mb-6">تسجيل الدخول</h1>
<SocialLogins />
<div className="relative my-6">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-300" />
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-white text-gray-500">
أو تابع باستخدام البريد الإلكتروني
</span>
</div>
</div>
<LoginForm />
</div>
)
}
<!-- app/login/social-logins.tsx -->
'use client'
import { signIn } from 'next-auth/react'
import { useState } from 'react'
export function SocialLogins() {
const [isLoading, setIsLoading] = useState<string | null>(null)
const handleSocialLogin = async (provider: string) => {
try {
setIsLoading(provider)
await signIn(provider, { callbackUrl: '/dashboard' })
} catch (error) {
console.error(`${provider} sign in error:`, error)
} finally {
setIsLoading(null)
}
}
return (
<div className="space-y-3">
<button
onClick={() => handleSocialLogin('google')}
disabled={isLoading === 'google'}
className="w-full flex items-center justify-center gap-3 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-50"
>
<GoogleIcon />
{isLoading === 'google' ? 'جاري التحميل...' : 'تابع مع Google'}
</button>
<button
onClick={() => handleSocialLogin('github')}
disabled={isLoading === 'github'}
className="w-full flex items-center justify-center gap-3 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-50"
>
<GitHubIcon />
{isLoading === 'github' ? 'جاري التحميل...' : 'تابع مع GitHub'}
</button>
</div>
)
}
<!-- app/login/login-form.tsx -->
'use client'
import { signIn } from 'next-auth/react'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
export function LoginForm() {
const router = useRouter()
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
setIsLoading(true)
setError(null)
const formData = new FormData(e.currentTarget)
const email = formData.get('email') as string
const password = formData.get('password') as string
try {
const result = await signIn('credentials', {
email,
password,
redirect: false,
})
if (result?.error) {
setError('بريد إلكتروني أو كلمة مرور غير صالحة')
return
}
router.push('/dashboard')
router.refresh()
} catch (error) {
setError('حدث خطأ ما. يرجى المحاولة مرة أخرى.')
} finally {
setIsLoading(false)
}
}
return (
<form onSubmit={handleSubmit} className="space-y-4">
{error && (
<div className="p-3 text-sm text-red-500 bg-red-50 rounded-lg">
{error}
</div>
)}
<div>
<label htmlFor="email" className="block text-sm font-medium mb-1">
البريد الإلكتروني
</label>
<input
id="email"
name="email"
type="email"
required
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium mb-1">
كلمة المرور
</label>
<input
id="password"
name="password"
type="password"
required
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<button
type="submit"
disabled={isLoading}
className="w-full px-4 py-2 text-white bg-blue-600 rounded-lg hover:bg-blue-700 disabled:opacity-50"
>
{isLoading ? 'جاري تسجيل الدخول...' : 'تسجيل الدخول'}
</button>
</form>
)
}
مسار API للتسجيل
أنشئ مسار API للتعامل مع تسجيل المستخدم:
<!-- app/api/register/route.ts -->
import { NextResponse } from 'next/server'
import bcrypt from 'bcryptjs'
import { prisma } from '@/lib/prisma'
export async function POST(request: Request) {
try {
const { name, email, password } = await request.json()
// التحقق من الصحة
if (!name || !email || !password) {
return NextResponse.json(
{ error: 'حقول مطلوبة مفقودة' },
{ status: 400 }
)
}
if (password.length < 8) {
return NextResponse.json(
{ error: 'يجب أن تكون كلمة المرور 8 أحرف على الأقل' },
{ status: 400 }
)
}
// التحقق من وجود المستخدم
const existingUser = await prisma.user.findUnique({
where: { email }
})
if (existingUser) {
return NextResponse.json(
{ error: 'المستخدم موجود بالفعل' },
{ status: 400 }
)
}
// تشفير كلمة المرور
const hashedPassword = await bcrypt.hash(password, 12)
// إنشاء المستخدم
const user = await prisma.user.create({
data: {
name,
email,
hashedPassword,
}
})
return NextResponse.json(
{ message: 'تم إنشاء المستخدم بنجاح', userId: user.id },
{ status: 201 }
)
} catch (error) {
console.error('خطأ في التسجيل:', error)
return NextResponse.json(
{ error: 'حدث خطأ ما' },
{ status: 500 }
)
}
}
حماية المسارات باستخدام Middleware
استخدم وسيط Next.js لحماية المسارات وإعادة توجيه المستخدمين غير المصادق عليهم:
<!-- middleware.ts -->
import { auth } from '@/lib/auth'
import { NextResponse } from 'next/server'
export default auth((req) => {
const isLoggedIn = !!req.auth
const isOnDashboard = req.nextUrl.pathname.startsWith('/dashboard')
const isOnAdminPanel = req.nextUrl.pathname.startsWith('/admin')
const isOnAuthPage = req.nextUrl.pathname.startsWith('/login') ||
req.nextUrl.pathname.startsWith('/register')
// إعادة التوجيه إلى تسجيل الدخول عند الوصول إلى المسارات المحمية دون تسجيل الدخول
if ((isOnDashboard || isOnAdminPanel) && !isLoggedIn) {
return NextResponse.redirect(new URL('/login', req.url))
}
// إعادة التوجيه إلى لوحة المعلومات عند الوصول إلى صفحات المصادقة أثناء تسجيل الدخول
if (isOnAuthPage && isLoggedIn) {
return NextResponse.redirect(new URL('/dashboard', req.url))
}
// التحقق من دور المسؤول لمسارات المسؤول
if (isOnAdminPanel && req.auth?.user?.role !== 'admin') {
return NextResponse.redirect(new URL('/dashboard', req.url))
}
return NextResponse.next()
})
export const config = {
matcher: ['/dashboard/:path*', '/admin/:path*', '/login', '/register']
}
الوصول إلى الجلسة من جانب الخادم
الوصول إلى بيانات الجلسة في مكونات الخادم ومسارات API:
<!-- app/dashboard/page.tsx (مكون الخادم) -->
import { auth } from '@/lib/auth'
import { redirect } from 'next/navigation'
export default async function DashboardPage() {
const session = await auth()
if (!session) {
redirect('/login')
}
return (
<div>
<h1>مرحباً، {session.user?.name}</h1>
<p>البريد الإلكتروني: {session.user?.email}</p>
</div>
)
}
<!-- app/api/user/profile/route.ts -->
import { auth } from '@/lib/auth'
import { NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
export async function GET() {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json(
{ error: 'غير مصرح' },
{ status: 401 }
)
}
const user = await prisma.user.findUnique({
where: { id: session.user.id },
select: {
id: true,
name: true,
email: true,
image: true,
createdAt: true,
}
})
return NextResponse.json({ user })
}
الوصول إلى الجلسة من جانب العميل
استخدم خطاف useSession في مكونات العميل:
<!-- app/components/user-menu.tsx -->
'use client'
import { useSession, signOut } from 'next-auth/react'
import { useRouter } from 'next/navigation'
export function UserMenu() {
const { data: session, status } = useSession()
const router = useRouter()
if (status === 'loading') {
return <div>جاري التحميل...</div>
}
if (!session) {
return (
<button onClick={() => router.push('/login')}>
تسجيل الدخول
</button>
)
}
return (
<div className="flex items-center gap-4">
<img
src={session.user?.image || '/default-avatar.png'}
alt={session.user?.name || 'المستخدم'}
className="w-8 h-8 rounded-full"
/>
<span>{session.user?.name}</span>
<button
onClick={() => signOut({ callbackUrl: '/' })}
className="text-red-600 hover:text-red-700"
>
تسجيل الخروج
</button>
</div>
)
}
التحكم في الوصول بناءً على الدور
قم بتنفيذ التفويض القائم على الدور للتحكم الدقيق في الوصول:
<!-- lib/auth-utils.ts -->
import { auth } from './auth'
import { redirect } from 'next/navigation'
export async function requireAuth() {
const session = await auth()
if (!session) {
redirect('/login')
}
return session
}
export async function requireAdmin() {
const session = await requireAuth()
if (session.user?.role !== 'admin') {
redirect('/dashboard')
}
return session
}
<!-- app/admin/page.tsx -->
import { requireAdmin } from '@/lib/auth-utils'
export default async function AdminPage() {
const session = await requireAdmin()
return (
<div>
<h1>لوحة تحكم المسؤول</h1>
<p>مرحباً، {session.user?.name}</p>
</div>
)
}
- قم بإعداد Auth.js مع موفري خدمات على الأقل (Google وبيانات الاعتماد)
- أنشئ صفحات التسجيل وتسجيل الدخول مع التحقق من الصحة الصحيح
- قم بتنفيذ وسيط لحماية مسارات لوحة المعلومات
- أضف التحكم في الوصول القائم على الدور لمسارات المسؤول
- أنشئ صفحة ملف تعريف المستخدم حيث يمكن للمستخدمين تحديث معلوماتهم
- قم بتنفيذ وظيفة إعادة تعيين كلمة المرور باستخدام روابط سحرية عبر البريد الإلكتروني
أفضل ممارسات إدارة الجلسة
- استخدم دائماً HTTPS في الإنتاج لحماية ملفات تعريف الارتباط للجلسة
- اضبط خيارات ملف تعريف الارتباط المناسبة (secure، httpOnly، sameSite)
- قم بتنفيذ مهلة الجلسة وآليات التحديث
- قم بتخزين الحد الأدنى من البيانات في JWTs للحفاظ على صغر حجمها
- استخدم جلسات قاعدة البيانات للتطبيقات الحساسة التي تتطلب إلغاء فوري
- قم دائماً بتشفير كلمات المرور باستخدام bcrypt (عامل التكلفة 12+)
- لا تسجل أو تعرض بيانات المصادقة الحساسة أبداً
الخلاصة
المصادقة في Next.js مبسطة مع Auth.js، حيث توفر OAuth وبيانات الاعتماد والمصادقة بدون كلمة مرور جاهزة. ادمج الحماية من جانب الخادم مع الوسيط، واستخدم الجلسات بشكل مناسب في كل من مكونات الخادم والعميل، ونفذ التحكم في الوصول القائم على الدور للتطبيقات الآمنة والجاهزة للإنتاج.