Der Unterschied in einer Tabelle
Beide Varianten sind in RFC 4648 definiert. Abschnitt 4 beschreibt Standard-Base64, Abschnitt 5 das Alphabet „URL and Filename safe“. Sie teilen 62 Zeichen und denselben Algorithmus; nur die Indizes 62 und 63 unterscheiden sich.
| Standard-Base64 (RFC 4648 §4) | Base64URL (RFC 4648 §5) | |
|---|---|---|
| Index 0–61 | A–Z a–z 0–9 | gleich |
| Index 62 | + | - |
| Index 63 | / | _ |
| Padding | = standardmäßig Pflicht | meist weggelassen |
| Unbedenklich in URL-Pfad oder Query | nein | ja |
| Unbedenklich als Dateiname | nein (/ trennt Pfade) | ja |
Hier dieselbe Eingabe auf beide Arten kodiert:
| Eingabe | Standard | Base64URL | Base64URL ohne Padding |
|---|---|---|---|
<<???>> | PDw/Pz8+Pg== | PDw_Pz8-Pg== | PDw_Pz8-Pg |
Bytes fb ff | +/8= | -_8= | -_8 |
Man | TWFu | TWFu | TWFu |
Die letzte Zeile erinnert daran, dass viele Zeichenketten in beiden Varianten gleich aussehen. Unterscheiden lässt sich eine Zeichenkette nur, wenn sie +, /, - oder _ enthält oder das Padding fehlt. Wenn Sie es nicht erkennen können, ist ein Decoder, der beide Alphabete akzeptiert, die sichere Wahl.
So bestimmen Sie schnell eine Zeichenkette, die Sie bekommen haben:
- Sie enthält
-oder_: Base64URL. - Sie enthält
+oder/: Standard-Base64. - Ihre Länge ist kein Vielfaches von 4, und sie hat kein
=: ohne Padding, was fast immer Base64URL bedeutet. - Sie enthält sowohl
+als auch-oder sowohl/als auch_: Irgendetwas hat die Alphabete vermischt, oder die Daten sind gar kein Base64. Der Base64-Validator zeigt, welche Zeichen fehl am Platz sind. - Sie hat drei durch Punkte getrennte Teile, die mit
eyJbeginnen: ein JWT, und das ist per Definition Base64URL.
Warum es Base64URL gibt
In einer URL trennt / Pfadsegmente, + bedeutet in formularkodierten Query-Strings ein Leerzeichen, und = trennt Schlüssel von Werten. Standard-Base64 verwendet alle drei. Um es in eine URL zu setzen, muss man es prozentkodieren, aus ab+/8= wird also ab%2B%2F8%3D: länger, hässlicher und ein weiterer Schritt, bei dem etwas schiefgehen kann.
RFC 4648 §5 behebt das mit - und _, die in URLs nicht reserviert und auf allen gängigen Dateisystemen in Dateinamen erlaubt sind. Das RFC merkt außerdem an, dass das Padding entfallen darf, wenn die Länge bekannt ist, da = selbst als %3D maskiert werden müsste. Mehr dazu in der Anleitung zum Padding.
Wo welche Variante verwendet wird
Standard-Base64:
- MIME-Anhänge und kodierte Header in E-Mails (Base64 in E-Mails)
- PEM-Zertifikate und -Schlüssel
data:-URIs in HTML und CSS- HTTP-Basic-Auth-Header
- Kubernetes Secrets und die meisten JSON-APIs, die Dateien transportieren
Base64URL, meist ohne Padding:
- JSON Web Tokens: Header, Payload und Signatur sind alle Base64URL ohne Padding (Base64 in JWT)
- JWK-Schlüssel, JWS und JWE allgemein (RFC 7515–7518)
- Credential-IDs und Challenges in WebAuthn
code_challenge-Werte von OAuth PKCE (RFC 7636)- Zufällige URL-Tokens wie Pythons
secrets.token_urlsafe(), das für 32 Zufallsbytes eine Zeichenkette mit 43 Zeichen geliefert hat
Zwischen beiden umwandeln
Die Umwandlung ist reine Zeichenersetzung, das Padding wird gesondert behandelt. Die Bytes ändern sich nie.
Standard zu URL-sicher: + durch - ersetzen, / durch _, und = am Ende entfernen.
URL-sicher zu Standard: - durch + ersetzen, _ durch /, und = anhängen, bis die Länge ein Vielfaches von 4 ist.
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=="
Die meisten Sprachen bringen auch einen direkten Encoder mit, die Zeichenjonglage ist also selten nötig:
| Sprache | Base64URL-Encoder | Padding |
|---|---|---|
| Python | base64.urlsafe_b64encode(b) | bleibt; mit .rstrip(b"=") entfernen |
| Node.js | buf.toString("base64url") | weggelassen |
| Go | base64.RawURLEncoding / base64.URLEncoding | weggelassen / bleibt |
| Java | Base64.getUrlEncoder().withoutPadding() | weggelassen |
| PHP | rtrim(strtr(base64_encode($s), '+/', '-_'), '=') | weggelassen |
Beim Dekodieren akzeptiert Nodes Buffer.from(s, "base64") beide Alphabete und fehlendes Padding. Pythons base64.urlsafe_b64decode braucht vorher das wiederhergestellte Padding. atob() im Browser lehnt - und _ mit einem InvalidCharacterError ab, wandeln Sie also vorher um oder nutzen Sie, wo verfügbar, Uint8Array.fromBase64 mit alphabet: "base64url". Code für andere Sprachen steht auf den Code-Seiten, zum Beispiel für Python und Java.
Wenn Sie nur eine einzelne Zeichenkette umstellen müssen, erledigen das Base64 in Base64URL und Base64URL in Base64 im Browser.
Fallstricke
+ wird zum Leerzeichen
Das ist der häufigste Fehler. Standard-Base64, das ohne Maskierung in einem Query-String landet, wird von der Formulardekodierung verstümmelt, die + als Leerzeichen liest:
from urllib.parse import parse_qs
parse_qs("t=ab+/8=")
# {'t': ['ab /8=']}
new URLSearchParams("t=ab+/8=").get("t");
// "ab /8="
Der nachgelagerte Decoder scheitert dann am Leerzeichen oder überspringt es und liefert falsche Bytes. Abhilfe schafft entweder Base64URL oder encodeURIComponent() auf dem Hinweg. Als schnelle Reparatur auf der Empfängerseite funktioniert es meist, Leerzeichen wieder durch + zu ersetzen, denn Base64 enthält nie Leerzeichen.
Stillschweigend vermischte Alphabete
Pythons base64.b64decode verwirft Zeichen außerhalb des Standardalphabets, solange Sie nicht validate=True übergeben. Bekommt es eine Base64URL-Zeichenkette, lässt es - und _ fallen und dekodiert den Rest:
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'<<???>>'
Der erste Aufruf liefert falsche Bytes, ohne einen Fehler auszulösen. Nehmen Sie urlsafe_b64decode für URL-sichere Eingaben oder validate=True, damit die Abweichung auffällt.
Doppelte Kodierung
Unter diesem Namen laufen zwei verschiedene Dinge, und beide kommen in der Praxis vor.
Doppelte Prozentkodierung. Eine Standard-Base64-Zeichenkette wird einmal von Ihrem Code und ein weiteres Mal von einem HTTP-Client maskiert, aus + wird also %2B und dann %252B. Der Server dekodiert einmal und erhält ein wörtliches %2B. Base64URL umgeht das Problem, weil es nichts zu maskieren gibt.
Base64 von Base64. Ein Wert wird auf zwei Ebenen kodiert, etwa von einer Anwendung und noch einmal von einem Framework oder von Tools für Kubernetes-Manifeste. Aus Man wird TWFu, dann VFdGdQ==. Das Erkennungszeichen: Das Dekodieren liefert etwas, das immer noch aus Base64-Zeichen besteht. Dekodieren Sie erneut und prüfen Sie, ob das Ergebnis Sinn ergibt.
Erwartungen ans Padding
Manche Empfänger verlangen Padding, andere lehnen es ab. Gos RawURLEncoding scheitert an Eingaben, die = enthalten, Pythons Decoder an Eingaben, denen es fehlt. Wenn Sie eine API definieren, nennen Sie Variante und Padding-Regel in der Dokumentation und lassen Sie Ihren eigenen Decoder beides akzeptieren.
Fügen Sie eine URL-sichere Zeichenkette ein, mit oder ohne Padding, und sehen Sie, was darin steckt.
Häufige Fragen
Ist Base64URL dasselbe wie eine URL-kodierte Base64-Zeichenkette?
Nein. Die Prozentkodierung macht aus + ein %2B und aus / ein %2F, wodurch die Zeichenkette länger wird. Base64URL verwendet ein anderes Alphabet, sodass gar keine Maskierung nötig ist.
Kann ein Standard-Base64-Decoder Base64URL lesen?
Nicht zuverlässig. Manche Decoder, etwa Buffer in Node, akzeptieren beide Alphabete. Andere lehnen - und _ ab, und Pythons b64decode verwirft sie standardmäßig stillschweigend, was falsche Bytes ergibt.
Hat Base64URL Padding?
RFC 4648 erlaubt es, und Pythons urlsafe_b64encode behält es bei. Die meisten Formate, die Base64URL verwenden, darunter JWT, lassen das = weg, weil die Länge es ohnehin festlegt.
Welche Variante sollte ich für Tokens in URLs verwenden?
Base64URL ohne Padding. Es braucht in Pfaden, Query-Strings, Cookies und Dateinamen keine Maskierung.