localStorage مقابل sessionStorage مقابل IndexedDB مقابل الكوكيز — تخزين المتصفح دون الشعور بالأسف في الساعة 3 صباحًا
دليل عملي لاختيار التخزين المناسب في المتصفح — مع جدول مقارن، وتحليل الجدل حول JWT بشكل واضح، وطرق محددة تؤثر سلبًا على نومك.
هُوَ 3 صباحًا 😬. قام مستخدم بتسجيل عطل: سلة التسوق فارغة. تفتح أدوات التطوير، تضغط على علامة التطبيق، ثم تنظر إلى الجانب الأيسر. أين أضعها؟ الإجابة الصحيحة تعتمد على عدد قليل من الأسئلة التي يفكر فيها معظم المطورين فقط بعد توثيق العطل.
هذا ليس مقالًا تعريفًا. يمكنك العثور عليه في أي مكان. هذا الجزء هو حيث نتحدث عن ما يُعطل، وما يجب استخدامه، وما السبب الذي يجعل مناقشة JWT في تخزين المواقع المحلية تُهمل النقطة الأساسية.
النسخة المختصرة (أو الجدول الذي سترتبه)
| localStorage | sessionStorage | IndexedDB | بسكويت | |
|---|---|---|---|---|
| الاستمرارية | مُستقرة | محدودة للجلسة | مُستقرة | قابلة للتعديل (جلسة أو تاريخ انتهاء) |
| الحد الأقصى للحجم | 5–10 ميغابايت | 5–10 ميغابايت | ميجابايت (محدودية المتصفح) | حوالي 4 كيلو بايت لكل ملف تبادل |
| الوصول إلى الخادم | لا | لا | لا | نعم — تُرسل مع كل طلب |
| واجهة برمجة تفاعلية | لا (تُوقف خيط الواجهة الرئيسية) | لا (تُوقف خيط الواجهة الرئيسية) | نعم (مبنية على التزامات أو الأحداث) | لا |
| قابلة للقراءة باللغة JavaScript | نعم | نعم | نعم | فقط بدون إشارة HttpOnly |
| الوصول إلى مُشغل الوسائط | لا | لا | نعم | لا |
| مُشتركة بين الأبواب | نعم | لا — كل علامة تُعزل | نعم | نعم |
| إزالة Safari ITP | بعد 7 أيام من عدم التفاعل | عند إغلاق البوابة | بعد 7 أيام من عدم التفاعل | يعتمد على خاصية الانتهاء |
localStorage
مُستقر، متوازن، محدود بالمصدر. المُستخدم الرئيسي الذي يُلجأ إليه من قبل الجميع، ونصف الوقت لا ينبغي أن يُستخدم.
ما هو في الحقيقة
تُخزن localStorage أزواج مفتاح-قيمة كنص. هذا كل شيء. الحد الأقصى للتخزين هو 5 ميغابايت في معظم المتصفحات، 10 ميغابايت في بعضها (يُعطي Chrome أكثر). يُحدد الحد بـ "المصدر" — البروتوكول + النطاق + المنفذ — لذا http://example.com و https://example.com يُحتفظ بسلاسل منفصلة. يُستمر بعد إغلاق البوابة، إعادة تشغيل المتصفح، كل شيء ما عدا تصفح المستخدم لبيانات المتصفح أو تفعيلك localStorage.clear().
// Read/write is synchronous — it happens right now, on the main thread
localStorage.setItem('theme', 'dark');
const theme = localStorage.getItem('theme'); // 'dark'
// Storing objects? You're serializing manually.
localStorage.setItem('user', JSON.stringify({ id: 1, name: 'Alex' }));
const user = JSON.parse(localStorage.getItem('user'));
أين تُعطل
- تمام القيود — يُنشئ خطأ متوازٍ
DOMException: QuotaExceededError. إذا لم تُغلف الكتابات بـ try/catch، ستجد ذلك من خلال تقرير عطل من المستخدم. - الخصوصية / الوضع المُخفٍ — يُعطي المتصفح لك تخزينًا جديدًا مُعزلًا بحد أقصى أصغر (أو صفر). كان Firefox يُنتج أخطاء القيود فورًا في الماضي. لا تُعتمد على توفر localStorage دون تحقق من ميزاته.
- Safari ITP — إذا لم يُزور المستخدم موقعك خلال 7 أيام، قد يُزيل Safari تخزين localStorage. هذا سلوك مُوثق. سيُفاجئك في أسوأ وقت.
- XSS — أي شيء في localStorage يمكن قراءته من قبل أي سكربت يعمل على مصدرك. إذا كان المهاجم يمكنه إدخال سكربت، فإنه يحصل على كل شيء.
استخدمه لـ
مُفضلات واجهة المستخدم (الوضع المظلم، حالة الجانب، اللغة)، تخزين غير حساس (وقت آخر تم التحميل، التكوين الثابت)، أي شيء يجب أن يُستمر بعد تجديد الصفحة ولكن لا يحتوي على بيانات التحقق أو بيانات شخصية. إذا كنت تفكر في وضع JWT أو مفتاح API هنا — ابقَ قريباً.
sessionStorage
كل ما يفعله localStorage، ولكن بفترة قصيرة وسلوك عزل واحد يُصيب الناس بشكل متكرر.
مفارقة عزل البوابة
يُعتبر sessionStorage مخصصًا لكل علامة، وليس لكل متصفح. فتح نفس الصفحة في علامة جديدة يعطيك تخزينًا منفصلًا. هذا لا مُبهر للمستخدمين، وستكون غير مُبهرة لك حتى تُقدَّم شكوى من المستخدم حول فقدان بيانات نموذج متعدد الخطوات عندما "أخطأ" في فتح علامة جديدة.
الاستثناء الوحيد: إذا فتح المستخدم علامة جديدة من خلال window.open() أو النقر بالزر الأوسط على رابط، فإن العلامة الجديدة تُحصل على نسخة من تخزين جلسة الوالد عند فتحها. بعد ذلك، تُعزل البوابة. هذا النوع من الحالات المحدودة يُنتج سؤالًا ممتازًا على موقع Stack Overflow في الساعة 2 صباحًا.
// Perfect for checkout flows — step data lives until the tab closes
sessionStorage.setItem('checkoutStep', '2');
sessionStorage.setItem('cartSnapshot', JSON.stringify(cart));
// Cleared automatically when the tab closes — no cleanup code needed
استخدمه لـ
مُعطى نموذج متعدد الخطوات، بيانات ورشة واحدة، أي شيء يجب أن يكون موجودًا لجلسة واحدة ويختفي عند إغلاق البوابة. هو مفيد حقًا في تدفق الدفع — لا ترغب في تثبيت حالة الدفع الجزئية من جلسة سابقة. لا تستخدمه إذا كنت بحاجة إلى بيانات مُشتركة بين الأبواب أو الصفحات المفتوحة بشكل مستقل.
IndexedDB
الخيار المُتطور. متوازٍ، مُتسلسل، وقادر على تخزين أشياء مبنية على JavaScript — وليس مجرد سلاسل مُسلسلة. كما أن لديها أبسط واجهة برمجة مُستخدمة من بين الثلاثة، وهي السبب الذي يجعله لا يُستخدم بشكل مباشر.
ما هو في الحقيقة
IndexedDB هو مخزن مفتاح-قيمة كامل مع دعم للإشارات والبحث باستخدام المُوجهات. الحدود المسموحة كبيرة — يسمح للمتصفح بسقف نسبة من المساحة المتوفرة، عادةً ميغابايت في الممارسة. يمكنك تخزين أشياء مبنية، ملفات، مصفوفات، وملفات دون الحاجة إلى تسلسل يدوي. يُتاح في مُشغل الوسائط. هو ما يستخدمه تطبيقات PWA وتطبيقات مُمكنة للعمل بدون اتصال لتخزين البيانات التي لا يمكن تحميلها في الذاكرة.
// Native IDB API — nobody writes this directly in production
const request = indexedDB.open('myDB', 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
db.createObjectStore('users', { keyPath: 'id' });
};
request.onsuccess = (event) => {
const db = event.target.result;
const tx = db.transaction('users', 'readwrite');
const store = tx.objectStore('users');
store.put({ id: 1, name: 'Alex', avatar: someBlob });
};
// What you actually use — Dexie.js or the idb wrapper
import Dexie from 'dexie';
const db = new Dexie('myApp');
db.version(1).stores({ users: '++id, name, email' });
await db.users.add({ name: 'Alex', email: 'alex@example.com' });
أين تُعطل
- Safari ITP — مثل localStorage: بعد 7 أيام من عدم التفاعل، يمكن أن يُزيل Safari بيانات IndexedDB. هذا أدى إلى تدمير عدة تطبيقات PWA قبل أن يُصلح Apple سلوكها في إصدارات iOS الجديدة. إذا كان جمهورك يشمل مستخدمي iOS Safari، فاعلم بذلك.
- محدودية التخزين على iOS — على iOS، يمكن أن يُزيل النظام بيانات IndexedDB عند نقص التخزين. لن يطلب. البيانات لن تكون موجودة.
- التصفح الخاص — يسمح Chrome بـ IndexedDB في التصفح الخاص (بحد مُحدد للجلسة). كان سلوك Safari في التصفح الخاص يُنتج أخطاء في الماضي؛ يختلف من إصدار إلى إصدار.
- الواجهة المُستخدمة — إذا كتبت كودًا خالصًا لـ IDB دون مُستند، فإن نموذج الأحداث المُعتمد على المُؤثر سيُنتج أخطاء ستفقد وقتًا طويلاً في التصحيح. استخدم idb أو Dexie.js.
استخدمه لـ
تطبيقات بدون اتصال، بيانات كبيرة تُستخدم لتخزينها في localStorage، أي شيء تُبني عليه localStorage إذا كان يُواجه حد 5 ميغابايت، تخزين الملفات في تطبيقات PWA. إذا كنت تُخزن 50 مستندًا مُنشأًا من قبل المستخدم محليًا، فإن IndexedDB هو الحل. إذا كنت تُخزن تفضيلات نمط المستخدم، فهو مفرط.
بسكويت
أقدم من بين الأربعة. هو الوحيد الذي يراه الخادم. هو الوحيد الذي يحتوي على حد 4 كيلو بايت والذي سيُضربك إذا حاولت وضع JWT فيه.
ما الذي يميز الكوكيز
يُرسل الكوكيز مع كل طلب HTTP مُطابق تلقائيًا. هذا هو الميزة والمشكلة. هذا يعني أن كوكيز الجلسة تصل إلى الخادم دون أي تدخل برمجي — ويعني أيضًا أن كل طلب إلى api.example.com يُحمل تكلفة الكوكيز سواء أحببت ذلك أم لا.
الخصائص التي تهم حقًا:
- يجب أن تكون أي أكواد تُستخدم لتحديد المستخدم لها — لا يمكن للجافاسكربت قراءة هذا الكوكيز. لا يمكن للاختراق عبر XSS استخراج هذا الكوكيز. هذا هو المعيار الأساسي لكوكيز الجلسة.
- يؤمن — يُرسل فقط عبر HTTPS. بدون هذا على موقع إنتاجي، فإنك تُرسل كوكيز التحقق عبر HTTP. لا تفعل ذلك.
- SameSite=Strict — يُرسل الكوكيز فقط عندما يبدأ الطلب من نطاقك. حماية فعالة من التهديدات عبر المواقع، لكنه يُوقف تدفق التوجيه عبر OAuth. استخدام SameSite=Lax هو توازن منطقي لمعظم التطبيقات.
- Expires / Max-Age — بدون هذه، سيكون كوكيز جلسة يُزيل عند إغلاق المتصفح. قم بتحديد تاريخ انتهاء لسلوك "تذكرني".
// Setting a cookie from JavaScript (no HttpOnly, obviously)
document.cookie = "theme=dark; Path=/; Max-Age=31536000; Secure; SameSite=Lax";
// Server-side (Node.js + Express) — where the real power is
res.cookie('sessionId', token, {
httpOnly: true, // JS cannot read this
secure: true, // HTTPS only
sameSite: 'lax', // balanced CSRF protection
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days in ms
});
إذا كنت تُحلل مشكلة كوكيز معقدة، فإن IO Tools’ Cookie Parser سيُحللها إلى أزواج مفتاح-قيمة قابلة للقراءة — مفيد عندما تنظر إلى رأس بطول 400 حرف وتحاول تحديد أي خاصية خاطئة. Set-Cookie الرأس
أين تُعطل
- 4 كيلو بايت — هذا هو الحجم الكلي بما في ذلك الاسم + القيمة + جميع الخصائص. يبلغ طول JWT عادةً 400–800 بايت. يبلغ طول الكوكيز الكثيف مع العديد من المطالبات 1–2 كيلو بايت. لديك مساحة محدودة.
- SameSite=Strict يُنهي OAuth — عندما يُعيد مزود الهوية التوجيه إلى تطبيقك بعد تسجيل الدخول، يكون هذا طلبًا عبر المواقع. يعني أن كوكيز الجلسة لن يُرسل. استخدم Lax لأي شيء يمر عبر تدفق OAuth.
- ترتيب الكوكيز وتوافق المسارات — عندما يكون هناك أكثر من كوكيز يحتوي على مسارات متقاطعة، فإن المتصفح يُرسلها في ترتيب غير محدد. لا تُعتمد على الأولوية.
- إلغاء الكوكيز المُستقل — يُزيل Chrome الكوكيز المُستقل. الاعتماد على الكوكيز عبر المواقع هو مخاطر متوسطة المدى.
مُناقشة JWT في التخزين المحلي (السطح الحقيقي للهجوم)
هذا يظهر في كل مناقشة حول التحقق، وغالبًا ما يتحدث الناس عن بعضهم دون فهم بعضهم.
القلق من تخزين JWT في localStorage: إذا كان موقعك يحتوي على ثغرة XSS، يمكن لسجّل المهاجم قراءة التوقيع مباشرة ونقله. الآن، يمتلك المهاجم توقيعًا صالحًا يمكنه استخدامه من أي مكان، على أي جهاز، حتى ينتهي.
الحالة المُفضلة لاستخدام كوكيز HttpOnly بدلًا من ذلك: لا يمكن للجافاسكربت قراءة الكوكيز، وبالتالي لا يمكن للاختراق عبر XSS نقله. الجلسة لا تزال قابلة للإستخدام في الطلبات (يمكن للمهاجم إرسال طلبات من متصفح الضحية عبر XSS)، لكنه لا يمكنه نقل التوقيع لاستخدامه في أماكن أخرى. هذا يحد من نطاق التأثير.
الرأي المُباشر: المشكلة الأساسية هي XSS، وليس مكان التخزين. إذا كان لديك ثغرة XSS، فإن كوكيز HttpOnly هو أفضل بكثير — لا يمكن للمهاجم نقل التوقيع خارج الموقع. لكن إصلاح XSS هو هدف أكثر أهمية، يمكن تحقيقه بسياسة أمان صارمة، عدم استخدام سكربتات غير موثوقة من مصادر خارجية، وتمثيل المخرجات بشكل صحيح.
إذا كنت تبني تطبيقًا SPA مع سياسة أمان صارمة وبدون سكربتات خارجية لا تتحكم فيها، فإن localStorage مناسب لـ JWT. إذا كنت تدير موقعًا يحتوي على Google Tag Manager، وملفات الإعلانات، وعشرات المكتبات npm، فإن كوكيز HttpOnly أكثر أمانًا لأن سطح هجومك عبر XSS أكبر مما تظن.
عند التحقق من مشاكل JWT، فإن JWT Decoder على IO Tools مفيد — قم بوضع التوقيع وشاهد المحتوى والانتهاء دون كتابة كود. فإن مُدقق انتهاء الصلاحية للـ JWT مفيد لتأكيد صلاحية التوقيع عندما تُتبع سلسلة 401.
مخطط القرار
قبل أن تفتح علامة Stack Overflow في الساعة 3 صباحًا، امرر هذا:
- هل يحتاج الخادم إلى قراءته؟ → كوكيز. نهاية المناقشة.
- هل هو أكبر من 5 ميغابايت أم أنك بحاجة إلى القدرة على الاستعلام؟ → IndexedDB.
- هل يجب أن يختفي عند إغلاق البوابة؟ → sessionStorage.
- الباقية → localStorage، مع الحذر المناسب حول ما تُخزن.
الشيء الوحيد الذي يخطئه الجميع
الواجهات المخزنة ليست موثوقة في جميع المستخدمين في جميع الأوقات. يمكن أن تُزيل من قبل المتصفح، نظام التشغيل، إعدادات الخصوصية، أو المستخدم. أي بنية تُعامل فيها التخزين المحلي كمصدر للحقيقة — بدلًا من كمصدر مُخزن — ستفشل في نهاية المطاف.
النمط الذي يُحافظ عليه: عامل التخزين المُستخدم كتحسين للسرعة مقارنة بمصدر الحقيقة الحقيقي (الخادم). اجعل التخزين مُتسرعًا، ولكن اصمم تطبيقك ليستعيد بسلاسة عند فراغ التخزين. تُعتبر جلسات التصحيح في الساعة 3 صباحًا تقريبًا ليست عن أي واجهة تخزين استخدمتها — بل عن افتراض أن البيانات ستكون موجودة عندما لم تكن.
قد يعجبك أيضاً
تثبيت ملحقاتنا
أضف أدوات IO إلى متصفحك المفضل للوصول الفوري والبحث بشكل أسرع
恵 وصلت لوحة النتائج!
لوحة النتائج هي طريقة ممتعة لتتبع ألعابك، يتم تخزين جميع البيانات في متصفحك. المزيد من الميزات قريبا!
