Base64, URL-кодирование и JWT: что это и как работает
Эти три вещи встречаются везде, где данные передаются в виде текста: в URL, HTTP-заголовках, JSON и токенах входа. Ни одна из них ничего не скрывает. В этом руководстве объясняется, как работает каждая, где вы с ней столкнётесь и какие ошибки вызывают большинство проблем.
Кодирование — не шифрование
Кодирование меняет запись данных так, чтобы они прошли через канал, принимающий только определённые символы. Отменить его может кто угодно, потому что ключ не используется. Шифрование тоже преобразует данные, но вернуть их может только владелец ключа. Хеш — это ещё одно: односторонний отпечаток, который невозможно обратить, как рассказано в руководстве Что такое хеш-функции.
Base64 и URL-кодирование — это кодирования. JWT обычно подписан, что доказывает, кто его выпустил и что он не изменён, но его содержимое лишь закодировано. Если значение должно оставаться секретным, ему нужно шифрование, а не один из этих форматов.
Как работает Base64
Base64 читает данные по три байта (24 бита) и делит их на четыре группы по шесть бит. Каждая группа — число от 0 до 63, которое записывается одним символом алфавита из 64 знаков: A–Z для 0–25, a–z для 26–51, 0–9 для 52–61, затем + и /. Слово «Man» показывает шаги:
| Шаг | M | a | n |
|---|---|---|---|
| Значения байтов | 77 | 97 | 110 |
| Биты | 01001101 | 01100001 | 01101110 |
| Четыре группы по 6 бит | 010011 · 010110 · 000101 · 101110 = 19 · 22 · 5 · 46 | ||
| Base64 | T W F u → TWFu | ||
Если длина входных данных не кратна трём, последняя группа дополняется и помечается заполнителем: один оставшийся байт даёт два символа и ==, два оставшихся байта — три символа и =. «Ma» превращается в TWE=, а «M» — в TQ==.
Поскольку каждые три байта становятся четырьмя символами, Base64 увеличивает данные примерно на треть, плюс переносы строк, которые добавляют некоторые форматы. Фото на 3 МБ, отправленное вложением в письме, передаётся как примерно 4 МБ текста, а изображение, встроенное в таблицу стилей как data-URL, весит примерно на треть больше файла.
Где встречается Base64
- Вложения электронной почты (MIME) — это текст Base64, разбитый на строки по 76 символов.
- Data-URL вида
data:image/png;base64,iVBORw0…встраивают небольшой файл прямо в HTML или CSS. - Базовая HTTP-аутентификация отправляет
user:passwordв Base64 в заголовке Authorization. Это скрывает пароль лишь от беглого взгляда, поэтому безопасно только по HTTPS. - Сертификаты и ключи в файлах PEM — это Base64 между строками
-----BEGIN …-----и-----END …-----, разбитый по 64 символа. - JSON и XML передают двоичные значения, например подпись или миниатюру, строками Base64, потому что двоичного типа в них нет.
Base64URL и заполнитель
Символы + и / стандартного Base64 имеют особое значение в URL и путях к файлам, поэтому второй алфавит, Base64URL, использует вместо них - и _. Обычно он также опускает заполнитель =, ведь декодер может вычислить длину сам. Base64URL используют JWT, многие API-токены и некоторые имена файлов. Чтобы превратить стандартный Base64 в Base64URL, замените + на -, / на _ и удалите = в конце; для обратного преобразования отмените замены и добавляйте =, пока длина не станет кратной четырём.
Процентное кодирование в URL
URL может содержать лишь ограниченный набор символов, и некоторые из них задают структуру адреса: / и ? отделяют путь и запрос, & и = разделяют параметры, # начинает фрагмент. Если значение содержит такой символ или что-либо за пределами ASCII, его кодируют в процентном виде: каждый байт его UTF-8-представления записывается как % и две шестнадцатеричные цифры. «café» становится caf%C3%A9, потому что é — это два байта C3 A9.
Буквы, цифры и - . _ ~ кодировать никогда не нужно. Чаще всего проблемы вызывают такие символы:
| Символ | В закодированном виде | Почему это важно |
|---|---|---|
| пробел | %20 | Недопустим в URL |
| & | %26 | Начал бы новый параметр |
| = | %3D | Завершил бы имя параметра |
| ? | %3F | Начал бы строку запроса |
| # | %23 | Начал бы фрагмент, который не отправляется на сервер |
| / | %2F | Добавил бы сегмент пути |
| + | %2B | В данных форм читается как пробел |
| % | %25 | Начал бы escape-последовательность |
Кодируйте каждое значение отдельно, а затем собирайте URL. В JavaScript encodeURIComponent делает это для одного значения и кодирует также / ? & = #, а encodeURI предназначен для адреса целиком и оставляет эти символы как есть. Применение encodeURI к отдельному значению — частая причина сломанных строк запроса.
Плюсы, пробелы и двойное кодирование
HTML-формы отправляют поля в формате application/x-www-form-urlencoded, где пробел превращается в +. В общем синтаксисе URL + — просто знак плюс. Поэтому поиск «C++», отправленный как q=C++, приходит на сервер, декодирующий данные форм, как «C» с двумя пробелами; отправлять его нужно как q=C%2B%2B. Когда вы декодируете строку запроса вручную, сначала выясните, по какому правилу она была создана.
Двойное кодирование возникает, когда уже закодированное значение кодируют ещё раз: %20 превращается в %2520, потому что сам знак % становится %25. Если в URL встречается %25 и за ним две шестнадцатеричные цифры, где-то в цепочке кодирование выполнено дважды; декодируйте один раз и найдите лишний шаг.
Что внутри JWT
JSON Web Token (JWT) переносит утверждения (claims) — например, кто пользователь и до какого момента токен действителен — от системы, которая его выпускает, к системам, которые его принимают. Распространённая подписанная форма состоит из трёх частей Base64URL, разделённых точками: заголовка, полезной нагрузки и подписи. После декодирования первые две — обычный JSON:
Header: {"alg": "HS256", "typ": "JWT"}
Payload: {"sub": "user-42", "role": "editor", "iat": 1767225600, "exp": 1767229200}
Стандарт (RFC 7519) определяет несколько зарегистрированных утверждений; приложения добавляют свои, как role выше.
| Утверждение | Значение |
|---|---|
| iss | Издатель: кто создал токен |
| sub | Субъект: обычно ID пользователя или учётной записи |
| aud | Аудитория: сервис, для которого предназначен токен |
| exp, nbf, iat | Истекает, не раньше, выпущен: Unix-время в секундах |
| jti | ID токена — чтобы обнаружить повторное использование или отозвать конкретный токен |
Поскольку полезная нагрузка лишь закодирована, прочитать её может любой, у кого есть токен. Не помещайте туда персональные данные и секреты и держите токены небольшими: они передаются с каждым запросом, а многие серверы ограничивают размер заголовков.
Подписан — не значит засекречен: как проверяют JWT
Подпись вычисляется по заголовку и полезной нагрузке. В HS256 издатель и получатель используют общий секрет и HMAC с SHA-256. В RS256 или ES256 издатель подписывает закрытым ключом, а получатели проверяют соответствующим открытым ключом, поэтому сами создавать токены не могут.
Сервер, получивший токен, должен проверить подпись ожидаемым алгоритмом и ключом, а также exp, nbf, iss и aud, прежде чем доверять какому-либо утверждению. Простое декодирование, как в инструменте LeedyBox, всё это пропускает: оно показывает, что написано в токене, но не говорит, подлинный ли он. Библиотеки также должны отклонять алгоритм «none», означающий неподписанный токен, если неподписанные токены не предусмотрены. Когда скрыть нужно само содержимое, токен шифруют (JWE); тогда он состоит из пяти частей, которые нельзя прочитать без ключа.
Типичные проблемы и решения
| Симптом | Вероятная причина | Решение |
|---|---|---|
| Base64 декодируется в бессмысленный текст | Данные двоичные или это текст в другой кодировке | Проверьте, что было закодировано; двоичные данные сохраняйте в файл |
| «Недопустимый символ» при декодировании | Префикс data:, кавычки или алфавит Base64URL | Удалите всё до запятой; используйте декодер, принимающий - и _ |
| Параметр приходит обрезанным | Незакодированный & или # в значении | Кодируйте каждое значение через encodeURIComponent |
| %2520 в URL | Значение закодировано дважды | Кодируйте один раз — там, где собирается URL |
| Токен отклонён, хотя exp ещё не наступил | Не та аудитория или издатель, расхождение часов или другой ключ | Сравните aud, iss и ключ подписи с тем, что ожидает сервер |
| Срок действия истекает через тысячи лет | exp записан в миллисекундах вместо секунд | Храните exp, nbf и iat в секундах |
Частые вопросы
Защищает ли Base64 пароль или API-ключ?
Нет. Base64 может обратить кто угодно за секунду. Учётные данные в Base64, например в файле конфигурации или в URL, так же открыты, как обычный текст.
Можно ли класть персональные данные в полезную нагрузку JWT?
Лучше избегать. Полезную нагрузку может прочитать любой, у кого есть токен, включая расширения браузера, журналы и прокси, видящие запрос. Кладите в токен идентификатор, а подробности получайте на сервере.
Защищает ли URL-кодирование от атак внедрения?
Только в том смысле, что значение не может сломать структуру URL. Сервер всё равно должен проверять и экранировать декодированное значение там, где использует его: в SQL, HTML или командах оболочки.
Почему строка Base64 длиннее файла?
Каждые три байта превращаются в четыре символа, поэтому текст примерно на 33 % больше, и переносы строк добавляют ещё немного. Большие файлы эффективнее передавать байтами, а не в Base64.