404 Not Found

404 Not Found


nginx

مصادقة OAuth2 + JWT

تحتاج Alice إلى إنشاء حساب لشراء المنتجات، ويحتاج Bob إلى تسجيل الدخول إلى لوحة الإدارة. لكن MegaShop لا يمتلك نظام مصادقة بعد—يمكن لأي شخص الوصول إلى واجهة الإدارة. يحتاج Charlie إلى دعم تسجيل البريد الإلكتروني وتسجيل الدخول الاجتماعي عبر Google/GitHub، مع ضمان أمان API.

1. ما ستتعلمه


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) مخطط تسلسل عملية المصادقة

100%
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)؛ عبء التحقق من القائمة السوداء عبر الوسيط ضئيل.

📖 ملخص


📝 تمارين

  1. تمرين أساسي (الصعوبة: ⭐): نفّذ API لتسجيل البريد الإلكتروني وتسجيل الدخول، باستخدام ملف تعريف ارتباط httpOnly لتخزين JWT
  2. تمرين متقدم (الصعوبة: ⭐⭐): نفّذ آلية رمز تحديث بحيث يتجدد access token تلقائياً عند انتهاء صلاحيته، دون أن يلاحظ المستخدم.
  3. تحدٍ (الصعوبة: ⭐⭐⭐): تكامل تسجيل الدخول الاجتماعي Google OAuth2 ونفّذ سير عمل كامل لتسجيل/دخول/تحديث/خروج

---|

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%