Base64 أم Base64URL: ما الذي يتغير ومتى تستخدم كلًّا منهما

Base64URL هو ترميز Base64 القياسي بعد استبدال محرفين، - بدل + و_ بدل /، وغالبًا دون الحشو بعلامة =.

كلا الترميزين معرّف في RFC 4648: ترميز Base64 القياسي في القسم 4، والنسخة «الآمنة للروابط وأسماء الملفات» (URL and filename safe)، أي base64url، في القسم 5. ويقسّم الاثنان البيانات إلى مجموعات من 6 بتات بالطريقة نفسها تمامًا، ويشتركان في 62 من أصل 64 محرفًا. لا يختلف إلا المحرفان اللذان يمثلان القيمتين 62 و63، إضافة إلى العرف المتّبع في الحشو. لذلك فالتحويل بينهما مجرد استبدال محارف دون إعادة ترميز البيانات، والخلط بينهما خطأ سهل الوقوع كثيرًا ما يمر دون أن يلاحظه أحد.

الأبجديتان جنبًا إلى جنب

Base64 (RFC 4648، القسم 4)Base64URL (RFC 4648، القسم 5)
القيم من 0 إلى 61A-Z a-z 0-9A-Z a-z 0-9
القيمة 62+-
القيمة 63/_
الحشو بـ =مطلوب، ما لم تنص الصيغة التي تستخدمه على غير ذلكيُحذف عادةً
آمن في الروابط وأسماء الملفات كما هولانعم

وهذه البايتات نفسها بالشكلين:

Bytes       fb ff bf
Base64      +/+/
Base64URL   -_-_

Text        <<???>>
Base64      PDw/Pz8+Pg==
Base64URL   PDw_Pz8-Pg

البيانات الثنائية المرمَّزة، كقيم التجزئة والمفاتيح والرموز العشوائية، تحتوي في أحيان كثيرة جدًا على + أو / في موضع ما، فالفرق ليس حالة هامشية. بل إن RFC 4648 نفسه ينص على أنه لا ينبغي اعتبار base64url الترميزَ نفسه الذي هو base64، وعلى أن الشيفرة يجب أن تسمّي النسخة التي تتوقعها.

لماذا ينكسر Base64 القياسي في الروابط وأسماء الملفات

لكل واحد من المحارف الثلاثة الإضافية في Base64 القياسي معنى خاص في مكان آخر:

  • + يُقرأ كمسافة حين تُفك سلسلة الاستعلام على أنها بيانات نموذج، وهذا ما تفعله URLSearchParams و$_GET في PHP ومعظم أطر عمل الخوادم. فالرمز ab+c مثلًا يصل على شكل ab c.
  • / يفصل مقاطع المسار، لذا تنشئ قيمة Base64 داخل المسار مقاطع إضافية، وبعض خوادم الويب يرفض الشكل المرمَّز %2F في المسارات. كما أنه غير مسموح في أسماء الملفات على Windows وmacOS وLinux.
  • = يفصل اسم المعامل عن قيمته.

ترميز القيمة بالنسبة المئوية (%2B و%2F و%3D) ينجح حين تفعل ذلك كل قطعة شيفرة معنية مرة واحدة بالضبط. لكن عمليًا تنسى إحدى الطبقات ذلك أو تفعله مرتين، ولهذا تستخدم الصيغ المصممة للروابط ترميز Base64URL وتتجنب التهريب كليًا.

قواعد الحشو

يرمّز Base64 كل 3 بايتات في 4 محارف. وحين لا يكون طول البيانات من مضاعفات 3 تكون المجموعة الأخيرة أقصر، فيكملها Base64 القياسي إلى 4 محارف بالعلامة =:

البايتات في المجموعة الأخيرةBase64Base64URL دون حشو
1، مثلًا ff/w==_w
2، مثلًا fb ff+/8=-_8
3، مثلًا fb ff bf+/+/-_-_

يشترط القسم 3.2 من RFC 4648 الحشو ما لم تنص المواصفة التي تشير إليه صراحةً على غير ذلك. ويضيف القسم 5 أن = سيحتاج عادةً إلى ترميز بالنسبة المئوية داخل الرابط، وأن الحشو يمكن الاستغناء عنه حين يكون طول البيانات معروفًا على أي حال. والصيغ المبنية على Base64URL تأخذ بهذا الخيار: تواقيع JSON Web Signature (RFC 7515)، ومن ثم رموز JWT، تحذف كل علامات = من النهاية، وكذلك تفعل تحديات الشيفرة في PKCE (RFC 7636).

ودون حشو تنتج n بايت عددًا من المحارف قدره ⌈4n/3⌉: فتصبح 16 بايت 22 محرفًا بدل 24، وتصبح 32 بايت 43 محرفًا بدل 44. أما الطول الذي يترك باقيًا قدره 1 عند قسمته على 4 فغير صالح أبدًا في أيٍّ من النسختين.

أين تُستخدم كل نسخة

السياقالنسخة
ترويسة JWT وحمولته وتوقيعهBase64URL دون حشو
قيم JSON Web Key مثل n وeBase64URL دون حشو
المعامل code_challenge في OAuth PKCEBase64URL دون حشو
روابط البيانات (Data URI)، data:image/png;base64,...Base64 القياسي (RFC 2397)
مرفقات البريد الإلكتروني (MIME)Base64 القياسي، بأسطر لا تتجاوز 76 محرفًا (RFC 2045)
شهادات ومفاتيح PEMBase64 القياسي، بأسطر من 64 محرفًا (RFC 7468)
ترويسة مصادقة HTTP BasicBase64 القياسي (RFC 7617)
الرموز في الروابط وأسماء الملفات ومفاتيح التخزين المؤقتBase64URL هو الخيار الآمن

تتبع الإعدادات الافتراضية للمكتبات ترميز Base64 القياسي: فالدالة btoa، وb64encode في Python، وbase64_encode في PHP، وBase64.getEncoder() في Java، كلها تنتج + و/ والحشو. وعليك أن تطلب Base64URL صراحةً.

التحويل بين الترميزين

لا تعرف atob وbtoa إلا Base64 القياسي، لذا عليك معهما أن تحوّل المحارف وتصلح الحشو بنفسك:

// Base64 -> Base64URL
const url = b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');

// Base64URL -> Base64
let std = url.replace(/-/g, '+').replace(/_/g, '/');
std += '='.repeat((4 - std.length % 4) % 4);

وتوفر المتصفحات الحديثة أيضًا Uint8Array.prototype.toBase64({ alphabet: 'base64url', omitPadding: true }) وUint8Array.fromBase64(s, { alphabet: 'base64url' })، وهما تعملان مباشرة على البايتات؛ لكن تحقق من دعمهما في المتصفحات التي تستهدفها قبل الاعتماد عليهما. أما المنصات الأخرى فلديها دعم مباشر أو صيغة قصيرة معتادة:

// Node.js
Buffer.from(data).toString('base64url');     // no padding
Buffer.from(s, 'base64url');

# Python
base64.urlsafe_b64encode(data).rstrip(b'=')
base64.urlsafe_b64decode(s + '=' * (-len(s) % 4))

// PHP
rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
base64_decode(strtr($s, '-_', '+/'), true);

// Java
Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
Base64.getUrlDecoder().decode(s);

التعليق no padding في سطر Node.js يعني أن الناتج يأتي دون حشو. وترفض urlsafe_b64decode في Python المدخلات التي ينقصها الحشو، ولهذا أُضيفت علامات =. أما base64_decode في PHP فتقبل الحشو الناقص، حتى في الوضع الصارم.

مزالق شائعة

  • أخطاء تجتاز الاختبارات. لا يفشل فاكّ ترميز Base64 القياسي مع مدخلات Base64URL إلا إذا احتوت على - أو _. وفي البيانات العشوائية يحدث ذلك في نحو نصف القيم ذات 16 بايت، ونحو ثلاثة أرباع القيم ذات 32 بايت مثل بصمات SHA-256 (1 − (62/64) مرفوعًا للقوة 42 ≈ 74%). لذا قد ينجح بسهولة اختبار يعتمد على قيمة أو قيمتين فقط. ولماذا يرفض فاكّ الترميز هذه النصوص، وكيف تختلف ردة فعل كل لغة، موضوعُ دليل خطأ Invalid Base64 string.
  • فاكّات الترميز المتساهلة تخفي الخلط. تقبل Buffer.from(s, 'base64') في Node.js الأبجديتين معًا، وهذا مريح لكنه يعني أن الخطأ لن يظهر إلا في خدمة أخرى. أما b64decode الافتراضية في Python فتُسقط - و_ بصمت، ثم تعيد بايتات خاطئة أو رسالة خطأ مضللة عن عدد المحارف.
  • Base64URL ليس ترميز روابط. ترميز نص Base64 قياسي بالنسبة المئوية يعطي %2B و%2F و%3D؛ ولا يحوّله إلى Base64URL، وفاكّ الترميز الذي يتوقع أحد الشكلين لن يقبل الآخر.
  • لا يخفي أيٌّ من الترميزين شيئًا. حمولة JWT ليست سوى JSON مرمّز بـ Base64URL، يقرؤها كل من يملك الرمز. الصق رمزًا في فاكّ ترميز JWT لترى ترويسته وحمولته.

ينتج مُرمِّز Base64 في هذا الموقع ترميز Base64 القياسي مع الحشو، فطبّق الاستبدال المذكور أعلاه إن كنت تحتاج إلى Base64URL. أما لفك الترميز فإن أداة فك ترميز Base64 تقبل الأبجديتين وتضيف الحشو الناقص بنفسها، فيمكنك أن تلصق أيًّا من الشكلين.

الأسئلة الشائعة

ما الفرق بين Base64 وBase64URL؟

يضع Base64URL العلامة - مكان + والعلامة _ مكان /، فتصبح النتيجة آمنة في الروابط وأسماء الملفات، وغالبًا يحذف الحشو بعلامة =. أما المحارف الـ 62 الأخرى وطريقة تقسيم البيانات إلى مجموعات من 6 بتات فمتطابقة.

هل يستطيع فاكّ ترميز Base64 العادي قراءة Base64URL؟

فقط بعد تحويله: ضع + مكان - و/ مكان _، ثم أضف = حتى يصبح الطول من مضاعفات 4. وبعض فاكّات الترميز، مثل Buffer في Node.js، تقبل الأبجديتين مباشرة.

لماذا تستخدم رموز JWT ترميز Base64URL دون حشو؟

لأن الرموز تنتقل في الروابط وترويسات HTTP وملفات تعريف الارتباط، حيث تسبب العلامات + و/ و= مشكلات. ويعرّف RFC 7515 الترميز المستخدم في JSON Web Signature بأنه base64url مع حذف كل علامات = من النهاية.

هل Base64URL أطول من Base64؟

لا. كلاهما ينتج عدد المحارف نفسه لكل بايت، ودون حشو يكون Base64URL أقصر بمحرفين على الأكثر.