Загрузить файл
  1. base64hub
  2. Статьи о Base64
  3. Base64 и Base64URL
Руководство

Base64 и Base64URL: алфавиты, дополнение, подводные камни

В Base64URL два символа Base64 заменены, чтобы результат можно было без изменений вставить в URL или имя файла. Байты те же самые, алфавиты различаются в двух позициях.

Разница в одной таблице

Оба варианта определены в RFC 4648. Раздел 4 описывает стандартный Base64, а раздел 5 алфавит, «безопасный для URL и имён файлов». У них 62 общих символа и один и тот же алгоритм, различаются только индексы 62 и 63.

Стандартный Base64 (RFC 4648 §4)Base64URL (RFC 4648 §5)
Индексы 0–61A–Z a–z 0–9те же
Индекс 62+-
Индекс 63/_
Дополнение= по умолчанию обязательнообычно опускается
Безопасен в пути или строке запроса URLнетда
Безопасен в имени файланет (/ разделяет пути)да

Вот один и тот же ввод, закодированный обоими способами:

ВводСтандартныйBase64URLBase64URL без дополнения
<<???>>PDw/Pz8+Pg==PDw_Pz8-Pg==PDw_Pz8-Pg
байты fb ff+/8=-_8=-_8
ManTWFuTWFuTWFu

Последняя строка показывает, что многие строки в обоих вариантах выглядят одинаково. Отличить их можно, только если в строке есть +, /, - или _ либо не хватает дополнения. Если понять нельзя, надёжнее всего взять декодер, который принимает оба алфавита.

Как быстро определить, что за строка вам досталась:

  • Есть - или _: Base64URL.
  • Есть + или /: стандартный Base64.
  • Длина не кратна 4 и нет =: строка без дополнения, а это почти всегда Base64URL.
  • Есть одновременно + и - или / и _: где-то смешали алфавиты, или это вообще не Base64. Валидатор Base64 покажет, какие символы стоят не на своём месте.
  • Три части через точку, первая начинается с eyJ: это JWT, а он по определению в Base64URL.

Зачем нужен Base64URL

В URL символ / разделяет сегменты пути, + в строке запроса из формы означает пробел, а = отделяет ключ от значения. Стандартный Base64 использует все три. Чтобы положить его в URL, придётся применять процентное кодирование: ab+/8= превращается в ab%2B%2F8%3D, а это длиннее, некрасивее и ещё один шаг, на котором можно ошибиться.

RFC 4648 §5 решает проблему, выбирая - и _: в URL они относятся к незарезервированным символам и допустимы в именах файлов на всех распространённых файловых системах. RFC также отмечает, что дополнение можно опустить, если длина известна, ведь = пришлось бы экранировать как %3D. Подробнее об этом рассказано в статье о дополнении.

Где используется каждый вариант

Стандартный Base64:

  • Вложения и закодированные заголовки в письмах MIME (Base64 в электронной почте)
  • Сертификаты и ключи PEM
  • data: URI в HTML и CSS
  • Заголовки HTTP Basic Auth
  • Секреты Kubernetes и большинство JSON API, которые передают файлы

Base64URL, обычно без дополнения:

  • JSON Web Token: заголовок, полезная нагрузка и подпись записаны в Base64URL без дополнения (Base64 в JWT)
  • Ключи JWK, JWS и JWE в целом (RFC 7515–7518)
  • Идентификаторы учётных данных и challenge в WebAuthn
  • Значения code_challenge в OAuth PKCE (RFC 7636)
  • Случайные токены для URL, например из secrets.token_urlsafe() в Python, который для 32 случайных байт вернул строку из 43 символов

Как конвертировать одно в другое

Конвертация сводится к замене символов и отдельной обработке дополнения. Сами байты не меняются.

Из стандартного в URL-safe: заменить + на -, / на _ и убрать завершающие =. Из URL-safe в стандартный: заменить - на +, _ на / и дописывать =, пока длина не станет кратной 4.

const toUrl = (b64) =>
  b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");

const fromUrl = (u) => {
  const b64 = u.replace(/-/g, "+").replace(/_/g, "/");
  return b64 + "=".repeat((4 - (b64.length % 4)) % 4);
};

toUrl("PDw/Pz8+Pg==");   // "PDw_Pz8-Pg"
fromUrl("PDw_Pz8-Pg");   // "PDw/Pz8+Pg=="

В большинстве языков есть и готовый кодировщик, так что жонглировать строками приходится редко:

ЯзыкКодировщик Base64URLДополнение
Pythonbase64.urlsafe_b64encode(b)сохраняется; убрать через .rstrip(b"=")
Node.jsbuf.toString("base64url")опускается
Gobase64.RawURLEncoding / base64.URLEncodingопускается / сохраняется
JavaBase64.getUrlEncoder().withoutPadding()опускается
PHPrtrim(strtr(base64_encode($s), '+/', '-_'), '=')опускается

При декодировании Buffer.from(s, "base64") в Node принимает оба алфавита и строки без дополнения. base64.urlsafe_b64decode в Python требует сначала вернуть дополнение. Браузерный atob() отклоняет - и _ с ошибкой InvalidCharacterError, поэтому сначала конвертируйте строку или, где это поддерживается, используйте Uint8Array.fromBase64 с alphabet: "base64url". Код для других языков собран на страницах с примерами, например для Python и Java.

Если нужно переключить одну строку, это делают в браузере инструменты Base64 в Base64URL и Base64URL в Base64.

Подводные камни

+ превращается в пробел

Это самая частая ошибка. Стандартный Base64, положенный в строку запроса без экранирования, портится при разборе формы, потому что + читается как пробел:

from urllib.parse import parse_qs
parse_qs("t=ab+/8=")
# {'t': ['ab /8=']}
new URLSearchParams("t=ab+/8=").get("t");
// "ab /8="

Дальше декодер либо падает на пробеле, либо пропускает его и возвращает не те байты. Решение: Base64URL или encodeURIComponent() при формировании URL. На принимающей стороне в качестве быстрой починки обычно помогает замена пробелов обратно на +: в Base64 пробелов не бывает.

Тихое смешение алфавитов

base64.b64decode в Python выбрасывает символы вне стандартного алфавита, если не передать validate=True. Получив строку Base64URL, он отбрасывает - и _ и декодирует остальное:

import base64
base64.b64decode("PDw_Pz8-Pg==")
# b'<<\x0f\xcf\xc3\xe0'   (wrong; the input was "<<???>>")
base64.b64decode("PDw_Pz8-Pg==", validate=True)
# binascii.Error: Only base64 data is allowed
base64.urlsafe_b64decode("PDw_Pz8-Pg==")
# b'<<???>>'

Ошибки нет, результат неверный. Для URL-safe ввода используйте urlsafe_b64decode или validate=True, чтобы несовпадение не прошло незамеченным.

Двойное кодирование

Под этим названием скрываются две разные вещи, и на практике встречаются обе.

Двойное процентное кодирование. Стандартную строку Base64 экранирует сначала ваш код, а потом ещё раз HTTP-клиент, так что + становится %2B, а затем %252B. Сервер декодирует один раз и получает буквальное %2B. Base64URL избавляет от проблемы: экранировать в нём нечего.

Base64 от Base64. Значение кодируется на двух уровнях, например приложением и потом ещё фреймворком или инструментами для манифестов Kubernetes. Man превращается в TWFu, а затем в VFdGdQ==. Признак такой: после декодирования получается строка, по-прежнему состоящая из символов Base64. Декодируйте ещё раз и посмотрите, осмыслен ли результат.

Ожидания насчёт дополнения

Одни получатели требуют дополнение, другие его отвергают. RawURLEncoding в Go падает на вводе с =, а декодер Python падает на вводе без него. Если вы проектируете API, укажите в документации вариант и правило дополнения, а свой декодер сделайте терпимым к обоим.

Декодировать Base64URL

Вставьте URL-safe строку с дополнением или без и посмотрите, что внутри.

Декодер Base64URL

Вопросы и ответы

Совпадает ли Base64URL с URL-кодированием строки Base64?

Нет. Процентное кодирование превращает + в %2B, а / в %2F, и строка становится длиннее. Base64URL использует другой алфавит, поэтому экранировать ничего не нужно.

Может ли обычный декодер Base64 прочитать Base64URL?

Не всегда. Некоторые декодеры, например Buffer в Node, принимают оба алфавита. Другие отклоняют - и _, а b64decode в Python по умолчанию молча их выбрасывает, и получаются неверные байты.

Есть ли в Base64URL дополнение?

RFC 4648 его допускает, и urlsafe_b64encode в Python его сохраняет. Большинство форматов на основе Base64URL, включая JWT, отбрасывают =, потому что длина и так однозначно его задаёт.

Что использовать для токенов в URL?

Base64URL без дополнения. Его не нужно экранировать ни в пути, ни в строке запроса, ни в cookie, ни в именах файлов.

Читайте также