مصادقة OAuth2 + JWT
تحتاج Alice إلى إنشاء حساب لشراء المنتجات، ويحتاج Bob إلى تسجيل الدخول إلى لوحة الإدارة. لكن MegaShop لا يمتلك نظام مصادقة بعد—يمكن لأي شخص الوصول إلى واجهة الإدارة. يحتاج Charlie إلى دعم تسجيل البريد الإلكتروني وتسجيل الدخول الاجتماعي عبر Google/GitHub، مع ضمان أمان API.
1. ما ستتعلمه
- تصميم هندسة المصادقة: مقارنة بين نهج Session وJWT وCookie
- تنفيذ JWT: الإصدار/التحقق + التخزين الآمن لملفات تعريف الارتباط httpOnly
- تكامل OAuth 2.0: عملية تسجيل الدخول الاجتماعي عبر Google/GitHub
- آلية رمز التحديث: Access Token + Refresh Token بالتناوب المزدوج
- تطبيق عملي لمصادقة MegaShop: تسجيل Alice ودخولها + Bob (المسؤول) OAuth2
2. قصة حقيقية لمهندس معماري
(1) نقطة الألم: ثغرة أمنية حرجة بسبب غياب المصادقة
يمكن لأي شخص قراءة بيانات سلة تسوق Alice؛ واجهة إدارة Bob تفتقر للمصادقة؛ واكتشف Charlie أن مهاجماً يمكنه تزوير طلب لحذف منتج. يجب أن يمتلك MegaShop نظام مصادقة.
(2) حل JWT + OAuth2
مصادقة JWT عديمة الحالة + تسجيل الدخول الاجتماعي OAuth 2.0: موازنة الأمان وتجربة المستخدم:
TYPESCRIPT
// الخادم: إصدار JWT بعد تسجيل الدخول
const token = jwt.sign({ userId: user.id, role: user.role }, secret, { expiresIn: '15m' })
setCookie(event, 'access-token', token, { httpOnly: true })
(3) الفوائد: أمان + سهولة
تستخدم Alice تسجيل الدخول عبر Google، ويصل Bob إلى لوحة الإدارة باستخدام حساب المسؤول، وتتضمن طلبات API رمز JWT للمصادقة التلقائية.
3. مقارنة مخططات المصادقة
(1) ثلاثة مخططات مصادقة
| البُعد | Session + Cookie | JWT + Cookie | JWT + localStorage |
|---|---|---|---|
| الحالة | ذات حالة (تخزين على الخادم) | عديمة الحالة | عديمة الحالة |
| قابلية التوسع | 🔴 تتطلب جلسات مشتركة | 🟢 موزعة بطبيعتها | 🟢 موزعة |
| توافق SSR | ✅ إرسال تلقائي لـ Cookie | ✅ إرسال تلقائي لـ Cookie | ❌ يتطلب معالجة يدوية |
| مخاطر CSRF | 🔴 تتطلب حماية | 🟢 محمية بـ httpOnly | 🟢 لا يوجد Cookie |
| مخاطر XSS | 🟢 httpOnly | 🟢 httpOnly | 🔴 قابل للقراءة عبر JS |
| تسجيل الخروج | ✅ حذف الجلسة فوراً | ⚠️ يتطلب قائمة سوداء | ⚠️ يتطلب قائمة سوداء |
💡 نصيحة: يستخدم MegaShop نهج JWT + ملف تعريف ارتباط httpOnly—الهندسة عديمة الحالة مناسبة للأنظمة الموزعة، وhttpOnly يمنع XSS، وملفات تعريف الارتباط تُضمَّن تلقائياً في طلبات SSR.
(2) مخطط تسلسل عملية المصادقة
sequenceDiagram
participant A as Alice/المتصفح
participant N as خادم Nuxt
participant G as Google OAuth2
participant DB as قاعدة البيانات
A->>N: النقر على "تسجيل الدخول عبر Google"
N->>G: إعادة التوجيه إلى شاشة موافقة Google
G-->>A: المستخدم يمنح الإذن
A->>N: استدعاء مع رمز التفويض
N->>G: استبدال الرمز بمعلومات المستخدم
G-->>N: ملف المستخدم الشخصي (البريد، الاسم)
N->>DB: إنشاء/إيجاد مستخدم
DB-->>N: سجل المستخدم
N->>N: توقيع JWT (access + refresh)
N-->>A: تعيين ملفات تعريف ارتباط httpOnly
A->>A: إعادة التوجيه إلى لوحة التحكم
4. إصدار JWT والتحقق منه
(1) ▶ مثال: دوال مساعدة لـ JWT
TYPESCRIPT
// server/utils/jwt.ts
import jwt from 'jsonwebtoken'
const config = useRuntimeConfig()
interface JwtPayload {
userId: number
email: string
role: 'customer' | 'admin'
}
export function signAccessToken(payload: JwtPayload): string {
return jwt.sign(payload, config.jwtAccessSecret, { expiresIn: '15m' })
}
export function signRefreshToken(payload: JwtPayload): string {
return jwt.sign(payload, config.jwtRefreshSecret, { expiresIn: '7d' })
}
export function verifyAccessToken(token: string): JwtPayload {
return jwt.verify(token, config.jwtAccessSecret) as JwtPayload
}
export function verifyRefreshToken(token: string): JwtPayload {
return jwt.verify(token, config.jwtRefreshSecret) as JwtPayload
}
الناتج:
TEXT
// التنفيذ ناجح
(2) ▶ مثال: API تسجيل البريد الإلكتروني
TYPESCRIPT
// server/api/auth/register.post.ts
export default defineEventHandler(async (event) => {
const { email, password, name } = await readBody(event)
if (!email || !password) {
throw createError({ statusCode: 400, message: 'Email and password required' })
}
// التحقق من وجود المستخدم
const existing = mockUsers.find(u => u.email === email)
if (existing) {
throw createError({ statusCode: 409, message: 'Email already registered' })
}
// إنشاء المستخدم
const user = {
id: mockUsers.length + 1,
email,
name: name || 'Customer',
password: await hashPassword(password),
role: 'customer' as const
}
mockUsers.push(user)
// إصدار الرموز
const payload = { userId: user.id, email: user.email, role: user.role }
const accessToken = signAccessToken(payload)
const refreshToken = signRefreshToken(payload)
// تعيين ملفات تعريف ارتباط httpOnly
setCookie(event, 'access-token', accessToken, {
httpOnly: true, secure: true, sameSite: 'lax', maxAge: 900 // 15 دقيقة
})
setCookie(event, 'refresh-token', refreshToken, {
httpOnly: true, secure: true, sameSite: 'lax', maxAge: 604800 // 7 أيام
})
return { user: { id: user.id, email: user.email, name: user.name, role: user.role } }
})
الناتج:
TEXT
// التنفيذ ناجح
(3) ▶ مثال: API تسجيل الدخول بالبريد الإلكتروني
TYPESCRIPT
// server/api/auth/login.post.ts
export default defineEventHandler(async (event) => {
const { email, password } = await readBody(event)
const user = mockUsers.find(u => u.email === email)
if (!user || !await verifyPassword(password, user.password)) {
throw createError({ statusCode: 401, message: 'Invalid credentials' })
}
const payload = { userId: user.id, email: user.email, role: user.role }
const accessToken = signAccessToken(payload)
const refreshToken = signRefreshToken(payload)
setCookie(event, 'access-token', accessToken, { httpOnly: true, secure: true, maxAge: 900 })
setCookie(event, 'refresh-token', refreshToken, { httpOnly: true, secure: true, maxAge: 604800 })
return { user: { id: user.id, email: user.email, name: user.name, role: user.role } }
})
الناتج:
TEXT
// التنفيذ ناجح
5. تسجيل الدخول الاجتماعي OAuth 2.0
(1) ▶ مثال: تسجيل الدخول عبر Google OAuth 2.0
TYPESCRIPT
// server/api/auth/google.get.ts
export default defineEventHandler((event) => {
const config = useRuntimeConfig()
const redirectUri = `${config.public.apiBase}/auth/google/callback`
const googleAuthUrl = `https://accounts.google.com/o/oauth2/v2/auth?` +
`client_id=${config.googleClientId}&` +
`redirect_uri=${redirectUri}&` +
`response_type=code&` +
`scope=openid email profile&` +
`state=${generateState()}`
return sendRedirect(event, googleAuthUrl)
})
الناتج:
TEXT
// التنفيذ ناجح
(2) ▶ مثال: معالجة استدعاء Google OAuth 2.0
TYPESCRIPT
// server/api/auth/google/callback.get.ts
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
const code = getQuery(event).code as string
// استبدال الرمز بالرموز
const tokenResponse = await $fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
body: {
code,
client_id: config.googleClientId,
client_secret: config.googleClientSecret,
redirect_uri: `${config.public.apiBase}/auth/google/callback`,
grant_type: 'authorization_code'
}
})
// الحصول على معلومات المستخدم من Google
const userInfo = await $fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
headers: { Authorization: `Bearer ${tokenResponse.access_token}` }
})
// إيجاد أو إنشاء مستخدم
let user = mockUsers.find(u => u.email === userInfo.email)
if (!user) {
user = {
id: mockUsers.length + 1,
email: userInfo.email,
name: userInfo.name,
avatar: userInfo.picture,
provider: 'google',
role: 'customer'
}
mockUsers.push(user)
}
// إصدار رموز JWT
const payload = { userId: user.id, email: user.email, role: user.role }
setCookie(event, 'access-token', signAccessToken(payload), { httpOnly: true, secure: true, maxAge: 900 })
setCookie(event, 'refresh-token', signRefreshToken(payload), { httpOnly: true, secure: true, maxAge: 604800 })
return sendRedirect(event, '/')
})
الناتج:
TEXT
// التنفيذ ناجح
6. آلية التناوب المزدوج للرموز
(1) دورة حياة الرمز
| الرمز | فترة الصلاحية | التخزين | الغرض |
|---|---|---|---|
| Access Token | 15 دقيقة | ملف تعريف ارتباط httpOnly | مصادقة API |
| Refresh Token | 7 أيام | ملف تعريف ارتباط httpOnly | تحديث Access Token |
(1) ▶ مثال: API تحديث الرمز
TYPESCRIPT
// server/api/auth/refresh.post.ts
export default defineEventHandler((event) => {
const refreshToken = getCookie(event, 'refresh-token')
if (!refreshToken) {
throw createError({ statusCode: 401, message: 'No refresh token' })
}
try {
const payload = verifyRefreshToken(refreshToken)
const newAccessToken = signAccessToken({
userId: payload.userId,
email: payload.email,
role: payload.role
})
setCookie(event, 'access-token', newAccessToken, {
httpOnly: true, secure: true, sameSite: 'lax', maxAge: 900
})
return { message: 'Token refreshed' }
} catch {
// انتهت صلاحية رمز التحديث - تسجيل دخول إجباري
setCookie(event, 'access-token', '', { maxAge: 0 })
setCookie(event, 'refresh-token', '', { maxAge: 0 })
throw createError({ statusCode: 401, message: 'Refresh token expired' })
}
})
الناتج:
TEXT
// التنفيذ ناجح
(2) ▶ مثال: وسيط مصادقة API
TYPESCRIPT
// server/middleware/auth.ts
export default defineEventHandler((event) => {
const url = getRequestURL(event)
if (!url.pathname.startsWith('/api/admin') && !url.pathname.startsWith('/api/cart')) return
const token = getCookie(event, 'access-token')
if (!token) {
throw createError({ statusCode: 401, message: 'Authentication required' })
}
try {
const payload = verifyAccessToken(token)
event.context.user = payload
// مسارات المسؤول فقط
if (url.pathname.startsWith('/api/admin') && payload.role !== 'admin') {
throw createError({ statusCode: 403, message: 'Admin access required' })
}
} catch {
throw createError({ statusCode: 401, message: 'Invalid or expired token' })
}
})
الناتج:
TEXT
// التنفيذ ناجح
7. مثال شامل: عملية مصادقة MegaShop
TYPESCRIPT
// nuxt.config.ts - أسرار JWT في runtimeConfig
export default defineNuxtConfig({
runtimeConfig: {
jwtAccessSecret: process.env.JWT_ACCESS_SECRET || 'dev-access-secret',
jwtRefreshSecret: process.env.JWT_REFRESH_SECRET || 'dev-refresh-secret',
googleClientId: process.env.GOOGLE_CLIENT_ID,
googleClientSecret: process.env.GOOGLE_CLIENT_SECRET,
public: {
apiBase: process.env.API_BASE || 'http://localhost:3000/api'
}
}
})
VUE
<!-- pages/login.vue -->
<template>
<div class="login-page">
<h1>Sign In to MegaShop</h1>
<!-- نموذج تسجيل الدخول بالبريد الإلكتروني -->
<form @submit.prevent="loginWithEmail">
<input v-model="email" type="email" placeholder="Email" required />
<input v-model="password" type="password" placeholder="Password" required />
<button type="submit">Sign In</button>
</form>
<!-- تسجيل الدخول الاجتماعي -->
<div class="social-login">
<p>Or sign in with:</p>
<a href="/api/auth/google" class="btn-google">Sign in with Google</a>
<a href="/api/auth/github" class="btn-github">Sign in with GitHub</a>
</div>
</div>
</template>
<script setup lang="ts">
const email = ref('')
const password = ref('')
async function loginWithEmail() {
await $fetch('/api/auth/login', {
method: 'POST',
body: { email: email.value, password: password.value }
})
navigateTo('/')
}
</script>
❓ أسئلة شائعة
س هل يجب تخزين JWT في ملف تعريف الارتباط أم في localStorage؟
ج نوصي باستخدام ملف تعريف ارتباط httpOnly—هذا يمنع هجمات XSS من سرقة الرمز، ويُضمَّن تلقائياً أثناء العرض من جانب الخادم (SSR). localStorage معرض لهجمات XSS.
س لماذا تدوم صلاحية Access Token 15 دقيقة فقط؟
ج فترة صلاحية أقصر تقلل من خطر تسريب الرمز. مع Refresh Token، يتجدد بسلاسة، لذلك لا يحتاج المستخدم لتسجيل الدخول بشكل متكرر.
س ماذا أفعل إذا تم اختراق رمز التحديث؟
ج طبّق تناوب رمز التحديث—في كل مرة تستخدم رمز تحديث للحصول على access token جديد، أصدر رمز تحديث جديد أيضاً، مما يجعل القديم تنتهي صلاحيته فوراً.
س ما الغرض من المعامل
state في OAuth 2.0؟ج لمنع هجمات CSRF.
state هو نص عشوائي يتم التحقق منه أثناء الاستدعاء لضمان أن الطلب قادم من تطبيقك وليس من مهاجم.س كيف تقرأ ملفات تعريف الارتباط للمصادقة أثناء SSR؟
ج
useCookie('access-token') يقرأ من عنوان ملف تعريف الارتباط في الطلب أثناء SSR ومن document.cookie على جانب العميل؛ القيم متسقة على الجانبين.س ماذا أفعل إذا تعذر إلغاء JWT تلقائياً؟
ج وقت انتهاء صلاحية قصير (15 دقيقة) يقلل التأثير. إذا كان الإلغاء الفوري مطلوباً، حافظ على قائمة سوداء للرموز (مخزنة في Redis)؛ عبء التحقق من القائمة السوداء عبر الوسيط ضئيل.
📖 ملخص
- يستخدم MegaShop رموز JWT + ملفات تعريف ارتباط httpOnly: عديمة الحالة، مقاومة لـ XSS، ومتوافقة مع SSR
- التناوب المزدوج للرموز: Access Token (15 دقيقة) + Refresh Token (7 أيام)، تحديث سلس
- تدفق OAuth 2.0: إعادة التوجيه إلى Google → تفويض المستخدم → الاستدعاء لاستبدال الرمز → إنشاء المستخدم → إصدار JWT
- مصادقة وسيط الخادم: قراءة JWT من ملف تعريف الارتباط → التحقق → الحقن في event.context.user
- خزّن جميع المفاتيح في حقل
runtimeConfigالخاص؛ لا تعرضها أبداً للعميل.
📝 تمارين
- تمرين أساسي (الصعوبة: ⭐): نفّذ API لتسجيل البريد الإلكتروني وتسجيل الدخول، باستخدام ملف تعريف ارتباط
httpOnlyلتخزين JWT - تمرين متقدم (الصعوبة: ⭐⭐): نفّذ آلية رمز تحديث بحيث يتجدد access token تلقائياً عند انتهاء صلاحيته، دون أن يلاحظ المستخدم.
- تحدٍ (الصعوبة: ⭐⭐⭐): تكامل تسجيل الدخول الاجتماعي Google OAuth2 ونفّذ سير عمل كامل لتسجيل/دخول/تحديث/خروج
---|



