Разница в одной таблице
Оба варианта определены в RFC 4648. Раздел 4 описывает стандартный Base64, а раздел 5 алфавит, «безопасный для URL и имён файлов». У них 62 общих символа и один и тот же алгоритм, различаются только индексы 62 и 63.
| Стандартный Base64 (RFC 4648 §4) | Base64URL (RFC 4648 §5) | |
|---|---|---|
| Индексы 0–61 | A–Z a–z 0–9 | те же |
| Индекс 62 | + | - |
| Индекс 63 | / | _ |
| Дополнение | = по умолчанию обязательно | обычно опускается |
| Безопасен в пути или строке запроса URL | нет | да |
| Безопасен в имени файла | нет (/ разделяет пути) | да |
Вот один и тот же ввод, закодированный обоими способами:
| Ввод | Стандартный | Base64URL | Base64URL без дополнения |
|---|---|---|---|
<<???>> | PDw/Pz8+Pg== | PDw_Pz8-Pg== | PDw_Pz8-Pg |
байты fb ff | +/8= | -_8= | -_8 |
Man | TWFu | TWFu | TWFu |
Последняя строка показывает, что многие строки в обоих вариантах выглядят одинаково. Отличить их можно, только если в строке есть +, /, - или _ либо не хватает дополнения. Если понять нельзя, надёжнее всего взять декодер, который принимает оба алфавита.
Как быстро определить, что за строка вам досталась:
- Есть
-или_: 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 | Дополнение |
|---|---|---|
| Python | base64.urlsafe_b64encode(b) | сохраняется; убрать через .rstrip(b"=") |
| Node.js | buf.toString("base64url") | опускается |
| Go | base64.RawURLEncoding / base64.URLEncoding | опускается / сохраняется |
| Java | Base64.getUrlEncoder().withoutPadding() | опускается |
| PHP | rtrim(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, укажите в документации вариант и правило дополнения, а свой декодер сделайте терпимым к обоим.
Вставьте URL-safe строку с дополнением или без и посмотрите, что внутри.
Вопросы и ответы
Совпадает ли Base64URL с URL-кодированием строки Base64?
Нет. Процентное кодирование превращает + в %2B, а / в %2F, и строка становится длиннее. Base64URL использует другой алфавит, поэтому экранировать ничего не нужно.
Может ли обычный декодер Base64 прочитать Base64URL?
Не всегда. Некоторые декодеры, например Buffer в Node, принимают оба алфавита. Другие отклоняют - и _, а b64decode в Python по умолчанию молча их выбрасывает, и получаются неверные байты.
Есть ли в Base64URL дополнение?
RFC 4648 его допускает, и urlsafe_b64encode в Python его сохраняет. Большинство форматов на основе Base64URL, включая JWT, отбрасывают =, потому что длина и так однозначно его задаёт.
Что использовать для токенов в URL?
Base64URL без дополнения. Его не нужно экранировать ни в пути, ни в строке запроса, ни в cookie, ни в именах файлов.