A diferença em uma tabela
As duas variantes estão definidas na RFC 4648. A seção 4 descreve o Base64 padrão, e a seção 5, o alfabeto “URL and Filename safe”. Elas compartilham 62 caracteres e o mesmo algoritmo; só os índices 62 e 63 mudam.
| Base64 padrão (RFC 4648 §4) | Base64URL (RFC 4648 §5) | |
|---|---|---|
| Índices 0–61 | A–Z a–z 0–9 | iguais |
| Índice 62 | + | - |
| Índice 63 | / | _ |
| Preenchimento | = obrigatório por padrão | geralmente omitido |
| Seguro em caminho ou query de URL | não | sim |
| Seguro como nome de arquivo | não (/ separa diretórios) | sim |
Aqui está a mesma entrada codificada dos dois jeitos:
| Entrada | Padrão | Base64URL | Base64URL, sem preenchimento |
|---|---|---|---|
<<???>> | PDw/Pz8+Pg== | PDw_Pz8-Pg== | PDw_Pz8-Pg |
bytes fb ff | +/8= | -_8= | -_8 |
Man | TWFu | TWFu | TWFu |
A última linha lembra que muitas strings ficam idênticas nas duas variantes. Só dá para distinguir uma string se ela contém +, /, - ou _, ou se falta o preenchimento. Quando não dá para saber, o mais seguro é um decodificador que aceite os dois alfabetos.
Um jeito rápido de identificar uma string que caiu nas suas mãos:
- Contém
-ou_: Base64URL. - Contém
+ou/: Base64 padrão. - O comprimento não é múltiplo de 4 e não há
=: sem preenchimento, o que quase sempre significa Base64URL. - Contém
+e-ao mesmo tempo, ou/e_: alguém misturou os alfabetos, ou os dados nem são Base64. O validador de Base64 mostra quais caracteres estão fora do lugar. - Tem três partes separadas por pontos começando com
eyJ: é um JWT, que por definição é Base64URL.
Por que o Base64URL existe
Numa URL, / separa segmentos do caminho, + significa espaço em query strings codificadas como formulário, e = separa chaves de valores. O Base64 padrão usa os três. Colocá-lo numa URL exige codificação percentual, então ab+/8= vira ab%2B%2F8%3D, que é mais longo, mais feio e mais um passo para dar errado.
A RFC 4648 §5 resolve isso escolhendo - e _, que não são reservados em URLs e são válidos em nomes de arquivo em todos os sistemas de arquivos comuns. A RFC também observa que o preenchimento pode ser omitido quando o comprimento é conhecido, já que o próprio = precisaria de escape como %3D. Mais sobre isso no guia de preenchimento.
Onde cada variante é usada
Base64 padrão:
- Anexos de e-mail MIME e cabeçalhos codificados (Base64 no e-mail)
- Certificados e chaves PEM
- URIs
data:em HTML e CSS - Headers de HTTP Basic Auth
- Secrets do Kubernetes e a maioria das APIs JSON que transportam arquivos
Base64URL, geralmente sem preenchimento:
- JSON Web Tokens: header, payload e assinatura são todos Base64URL sem preenchimento (Base64 no JWT)
- Chaves JWK, JWS e JWE em geral (RFC 7515–7518)
- IDs de credencial e challenges do WebAuthn
- Valores
code_challengedo OAuth PKCE (RFC 7636) - Tokens aleatórios para URLs, como o
secrets.token_urlsafe()do Python, que retornou uma string de 43 caracteres para 32 bytes aleatórios
Como converter entre eles
A conversão é pura substituição de caracteres, com o preenchimento tratado à parte. Os bytes nunca mudam.
Do padrão para URL-safe: troque + por -, / por _ e remova os = do final.
De URL-safe para o padrão: troque - por +, _ por / e acrescente = até o comprimento ser múltiplo de 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=="
A maioria das linguagens também traz um codificador direto, então raramente é preciso fazer esse malabarismo com strings:
| Linguagem | Codificador Base64URL | Preenchimento |
|---|---|---|
| Python | base64.urlsafe_b64encode(b) | mantido; remova com .rstrip(b"=") |
| Node.js | buf.toString("base64url") | omitido |
| Go | base64.RawURLEncoding / base64.URLEncoding | omitido / mantido |
| Java | Base64.getUrlEncoder().withoutPadding() | omitido |
| PHP | rtrim(strtr(base64_encode($s), '+/', '-_'), '=') | omitido |
Na decodificação, o Buffer.from(s, "base64") do Node aceita os dois alfabetos e o preenchimento ausente. O base64.urlsafe_b64decode do Python precisa que o preenchimento seja restaurado antes. O atob() do navegador rejeita - e _ com um InvalidCharacterError, então converta antes ou use Uint8Array.fromBase64 com alphabet: "base64url" onde estiver disponível. Código para outras linguagens está nas páginas de código, por exemplo Python e Java.
Se você só precisa converter uma string, Base64 para Base64URL e Base64URL para Base64 fazem isso no navegador.
Armadilhas
O + vira espaço
Esse é o bug mais comum. Base64 padrão colocado numa query string sem escape é estragado pela decodificação de formulário, que lê + como espaço:
from urllib.parse import parse_qs
parse_qs("t=ab+/8=")
# {'t': ['ab /8=']}
new URLSearchParams("t=ab+/8=").get("t");
// "ab /8="
O decodificador seguinte então falha no espaço, ou o ignora e devolve os bytes errados. A correção é usar Base64URL, ou encodeURIComponent() na ida. Como conserto rápido do lado de quem recebe, trocar os espaços de volta por + costuma funcionar, porque Base64 nunca contém espaços.
Misturar alfabetos sem perceber
O base64.b64decode do Python descarta caracteres fora do alfabeto padrão, a menos que você passe validate=True. Se receber uma string Base64URL, ele joga fora os - e _ e decodifica o resto:
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'<<???>>'
A primeira chamada devolve bytes errados sem levantar nenhum erro. Use urlsafe_b64decode para entrada URL-safe, ou validate=True para que a incompatibilidade fique evidente.
Dupla codificação
Duas coisas diferentes recebem esse nome, e as duas acontecem na prática.
Codificação percentual duas vezes. Uma string Base64 padrão recebe escape uma vez no seu código e mais uma vez num cliente HTTP, então + vira %2B e depois %252B. O servidor decodifica uma vez e recebe um %2B literal. O Base64URL evita o problema porque não há nada para escapar.
Base64 de Base64. Um valor é codificado em duas camadas, por exemplo pela aplicação e de novo por um framework ou pelas ferramentas de manifesto do Kubernetes. Man vira TWFu e depois VFdGdQ==. O sinal é que a decodificação devolve algo que continua sendo feito de caracteres Base64. Decodifique de novo e veja se o resultado faz sentido.
Expectativas sobre preenchimento
Alguns receptores exigem preenchimento e outros o rejeitam. O RawURLEncoding do Go falha com entrada que contém =, enquanto o decodificador do Python falha com entrada sem ele. Ao definir uma API, documente a variante e a regra de preenchimento, e faça seu próprio decodificador aceitar as duas formas.
Cole uma string URL-safe, com ou sem preenchimento, e veja o que tem dentro.
Perguntas frequentes
Base64URL é o mesmo que aplicar URL encoding numa string Base64?
Não. A codificação percentual transforma + em %2B e / em %2F, o que deixa a string mais longa. O Base64URL usa outro alfabeto justamente para que nenhum escape seja necessário.
Um decodificador Base64 padrão consegue ler Base64URL?
Não de forma confiável. Alguns decodificadores, como o Buffer do Node, aceitam os dois alfabetos. Outros rejeitam - e _, e o b64decode do Python os descarta silenciosamente por padrão, o que produz bytes errados.
O Base64URL tem preenchimento?
A RFC 4648 permite, e o urlsafe_b64encode do Python o mantém. A maioria dos formatos que usam Base64URL, incluindo o JWT, descarta o = porque o comprimento já o determina.
Qual devo usar para tokens em URLs?
Base64URL sem preenchimento. Ele não precisa de escape em caminhos, query strings, cookies nem nomes de arquivo.