Haben Sie mit dem Base64-Format zu tun? Dann ist diese Website genau das Richtige für Sie! Nutzen Sie unser superpraktisches Online-Tool, um Ihre Daten zu kodieren oder zu dekodieren.

Base64-Dekodierung in Java: Ein vollständiger Leitfaden

Er kommt im Support-Ticket an, in einer API-Antwort, in einem Kubernetes-Secret, oder er ist mitten in einer URL vergraben: ein langer Zug aus Buchstaben und Ziffern mit dem gelegentlichen +, /, - oder _, und vielleicht noch ein oder zwei =-Zeichen, die am Ende baumeln. Jemand sagt, es sei Base64 und es enthalte etwas, das Sie brauchen: ein Passwort, eine JSON-Nutzlast, ein Zertifikat, ein Foto. Dieser Leitfaden ist das Java-Rezept, es wieder zurückzubekommen. Kurze Orientierung, denn die Startseite geht das Format ausführlich durch: Base64 schreibt jeweils drei Bytes der Daten als vier Zeichen aus einem Alphabet von 64 Buchstaben um und hängt ein oder zwei =-Pads ans Ende, wenn das letzte Stück kurz ist. Dekodieren ist die schrumpfende Richtung dieses Tauschs: Vier Zeichen gehen rein, drei Bytes kommen raus, also braucht das Ergebnis immer etwa ein Viertel weniger Platz als die Eingabe.

Das ist die Schlagzeile, und es ist eine gute. Seit dem 18. März 2014 liefert jedes JDK ein komplettes Base64-Werkzeugset in der Standardbibliothek mit: java.util.Base64. Kein Download, keine Maven-Koordinate, keine native Bibliothek. Ein Import, sieben Fabrikmethoden, drei Alphabete, und dasselbe Verhalten von Java 8 bis zum heutigen Java 26. Alles in diesem Artikel baut auf diese eine Klasse auf.

Eine ehrliche Grenze, bevor wir starten: Dies ist die Dekoder-Seite der Geschichte. Sie lernen, den richtigen Dekodierer für das Alphabet auszuwählen, auf das Sie treffen, die Fehlermeldungen des JDK zu lesen wie ein Arzt eine Röntgenaufnahme, Bytes in Text zu verwandeln ohne Mojibake, PEM-Armor zu entfernen, Nutzlasten von mehreren Gigabyte zu streamen und die Sicherheitsfallen zu erkennen, die das Format unauffällig auf dem Weg liegen lässt. Die andere Richtung, Bytes in einen String zu packen, bekommt ihren eigenen Leitfaden, und der ist am Ende dieses hier verlinkt.

Was Sie bereits besitzen

Base64 in Java zu installieren ist die Einzeiler-Antwort, die Sie am Whiteboard geben: "Es ist im JDK." Die Klasse java.util.Base64 ist seit 1.8 Teil des java.base-Moduls, und ihre Javadoc sagt 2026 immer noch Since: 1.8. Das Einzige, was Sie installieren, ist ein JDK; jedes Java 8 oder neuer von jedem Anbieter (Oracle, Eclipse Temurin, Amazon Corretto, Zulu) funktioniert, und auf einer Debian-basierten Box ist das ein einzelner Befehl:

sudo apt install openjdk-17-jdk-headless

Die API ist eine Fabrik: Sie konstruieren nie einen Dekodierer; Sie bitten die Klasse um einen. Die sieben Fabrikmethoden geben in jede Richtung drei Persönlichkeiten aus, und die Dekoder-Seite sieht so aus:

Fabrikmethode Alphabet Stimmung Greifen Sie danach, wenn
getDecoder() A-Z a-z 0-9 + / Streng: lehnt jedes Zeichen außerhalb des Alphabets ab Daten, die Sie erzeugen oder kontrollieren
getUrlDecoder() A-Z a-z 0-9 - _ Streng, URL-sicheres Alphabet JWTs, Tokens, IDs, alles, was in einer URL geboren wurde
getMimeDecoder() A-Z a-z 0-9 + / Tolerant: überspringt jedes Zeichen außerhalb des Alphabets E-Mail, wirklich umhüllte Eingabe, PEM-Körper
getEncoder(), getUrlEncoder(), getMimeEncoder() wie oben Kodierung, das Terrain des Schwester-Leitfadens Wann immer Sie Base64 erzeugen statt es zu lesen

Drei Eigenschaften der zurückgegebenen Instanzen lohnen es, sie zu merken. Erstens sind sie threadsicher: Die Javadoc sagt, Instanzen seien "geeignet für die Nutzung durch mehrere gleichzeitige Threads", und der Quellcode zeigt, dass die Fabrikmethoden bei jedem Aufruf dieselbe gemeinsame Instanz zurückgeben, also ist Base64.getDecoder() == Base64.getDecoder() wahr. Erstellen Sie einen Dekodierer in einem statischen Feld und teilen Sie ihn über Ihren gesamten Service; Sie kopieren dabei nicht einmal etwas. Zweitens sind sie zwischen den Aufrufen zustandslos, es gibt also nichts zurückzusetzen und nichts zu synchronisieren. Drittens ist es kein sanftes Nichts, null zu übergeben, wo ein Byte-Array oder ein String erwartet wird: Es ist eine NullPointerException, genau wie es die Javadoc der Klasse verspricht.

Sie werden in Codebasen trotzdem ältere Bibliotheken treffen, also hier eine schnelle Landkarte des Geländes. Apache Commons Codec (derzeit 1.22.1) trägt seit 1.0 sein eigenes org.apache.commons.codec.binary.Base64 mit, mit einer Builder-API, die die strenge oder tolerante Ausrichtung, die Zeilenlänge und den Trenner als Regler offenlegt; ist es das richtige Werkzeug nur, wenn Sie JVMs von vor Java 8 unterstützen müssen oder seine Form-Prüfungshelfer wollen. Guava liefert com.google.common.io.BaseEncoding, einen ebenso fähigen Veteranen, der in Big-Data-Stacks immer noch beliebt ist. Für alles, was auf einer modernen JVM läuft, ist java.util.Base64 die Standardwahl: null Abhängigkeiten, und Community-Benchmarks halten es immer wieder für das schnellste aller Kandidaten (mehr dazu in der Performance-Sektion).

Den ersten String dekodieren

Neunzig Prozent des Dekodier-Alltags passen in ein paar Zeilen. Hier ist die ganze Zeremonie, mit dem kleinsten Beispiel, das der RFC selbst verwendet, um das Alphabet zu erklären:

import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class FirstDecode {
  public static void main(String[] args) {
    byte[] bytes = Base64.getDecoder().decode("TWFu");
    String text = new String(bytes, StandardCharsets.UTF_8);
    System.out.println(text); // Man
  }
}

Vier Sätze über das, was gerade passiert ist. Erstens: Der Einstieg ist eine Instanz, nicht die Klasse: decode() lebt auf dem Base64.Decoder-Objekt, das Sie aus der Fabrik bekommen haben. Zweitens, und das ist die wichtigste Designentscheidung der gesamten API: Das Ergebnis ist ein Byte-Array, niemals ein String. Die Nutzlast könnte ein Satz sein, ein JPEG oder ein Hash, und keines davon sollte gleich behandelt werden, bevor Sie wissen, was Sie haben, also hält das JDK bewusst bei den Bytes an. Drittens: Der Sprung von Bytes zu Text ist ein eigener, bewusster Schritt mit einem expliziten Zeichensatz, und genau an diesem Schritt wird aus "café" Mojibake, wenn Sie nachlässig sind; die Zeichensatz-Sektion unten ist ihm gewidmet. Viertens: Der leere String ist ein Wert erster Klasse: Base64.getDecoder().decode("") liefert Ihnen ein Array der Länge null, keine Exception, kein Drama.

Als Testdaten im Kopf merken Sie: TWFu ist der Smoke-Test des Standards selbst. Wenn Ihr Dekodier-Code daraus Man macht, ist die Maschine ehrlich. Die Rückfahrt in die andere Richtung besteht aus zwei Zeilen derselben API und bekommt die volle Behandlung im Kodierungs-Leitfaden, der am Ende verlinkt ist.

Das Dekodierer-Aufgebot

Java gibt Ihnen nicht einen Dekodierer; es gibt Ihnen drei, und der Unterschied zwischen ihnen ist eine Politikentscheidung darüber, welches Alphabet akzeptiert und welches Maß an Unordnung toleriert wird. Alle drei sind Instanzen derselben verschachtelten Klasse Base64.Decoder. Die Javadoc der Klasse legt die Aufteilung in je einem Satz pro Stimmung fest. Für die Basis- und die URL-sicheren Dekodierer: der Dekodierer "lehnt Daten ab, die Zeichen außerhalb des base64-Alphabets enthalten". Für den MIME-Dekodierer: "Alle Zeilentrenner oder anderen Zeichen, die in der base64-Alphabettabelle nicht zu finden sind, werden im Dekodierungsvorgang ignoriert". Der zweite Satz ist die gesamte MIME-Geschichte in einer Zeile, und er hat Zähne, denn "ignored" bedeutet alles, was kein Alphabetzeichen ist, nicht nur Zeilenumbrüche.

Die Auswahlregel ist kurz. Standardmäßig getDecoder(). Wenn der Wert aus einer URL, einem Token oder einer API kam, die "URL-safe" versprach, wechseln Sie zu getUrlDecoder(). Nur wenn Sie wirklich MIME-geformte Eingabe erwarten (Zeilenumbrüche alle 76 Zeichen, direkt aus einem Mailsystem) greifen Sie nach getMimeDecoder(). Im Zweifel: streng wählen. Die Aufgabe eines strengen Dekodierers ist es, Überraschungen scheitern zu lassen, und genau das wollen Sie an einer Vertrauensgrenze. Ein toleranter Dekodierer hingegen ist eine Lupe für Korruption: Ein String mit eingeschlichenen Zeichen dekodiert zu etwas, das plausibel und falsch zugleich ist, und zwar ohne jeden Fehler.

Die Beschwerden des Dekodierers lesen

Die strengen Dekodierer scheitern laut, und sie scheitern präzise. Jede schlechte Eingabe wirft eine IllegalArgumentException, deren Meldung Ihnen genau sagt, was schiefgelaufen ist, also ist diese Tabelle das, was Sie lesen, wenn zum ersten Mal ein Produktiv-String explodiert. Die Meldungen unten sind die exakte Wortwahl des aktuellen JDK:

Eingabe (an getDecoder, sofern nicht anders angegeben) Was falsch ist Exakte Meldung
"SGVs bG8s" ein Leerzeichen hat sich eingeschlichen Illegal base64 character 20
"SGVs\nbG8s" ein Zeilenumbruch hat sich eingeschlichen Illegal base64 character a
"SGVs$bG8s" ein Dollarzeichen ist nicht im Alphabet Illegal base64 character 24
"SGVsbG8-" ein URL-sicherer Gedankenstrich im Standard-Dekodierer Illegal base64 character 2d
"ab+c" an getUrlDecoder() ein Pluszeichen im URL-sicheren Dekodierer Illegal base64 character 2b
"S" ein Symbol kann kein Byte bilden Input byte[] should at least have 2 bytes for base64 bytes
"SG=VsbG8s" Padding in der Mitte der Daten Input byte array has wrong 4-byte ending unit
"Zm8==" zwei Pads, wo eines hingehört Input byte array has incorrect ending byte at 4
"Z=" ein Zeichen, gefolgt von einem Pad Last unit does not have enough valid bits
"SGVsbG8sIHdvcmxkIQ==xx" Müll nach den Pads Input byte array has incorrect ending byte at 20

Das Hexadezimal in der Meldung ist der Byte-Wert des Übeltäters, ausgedruckt mit Integer.toString(byte, 16): 20 ist ein Leerzeichen, a ein Zeilenvorschub, d ein Wagenreturn, 24 ein Dollarzeichen, 2d der URL-sichere Gedankenstrich, 2b das Plus, 2f der Schrägstrich, 5f der Unterstrich. Zwei Kuriositäten für die Hosentasche. Erstens kann die Meldung negativ werden: Füttern Sie den Dekodierer mit einem String, der é enthält, und er beschwert sich mit Illegal base64 character -17, denn das Zeichen wird zuerst auf das Latin-1-Byte 0xE9 abgebildet, das als signiertes Java-Byte minus 23 ist, und minus 23 im Hexadezimal ist minus 17. Ihr Fehler-Logger treibt für einen Moment signierte Arithmetik. Zweitens die Position: In der incorrect ending byte at N-Familie ist N der nullbasierte Index des ersten Bytes, das der Dekodierer nicht einordnen konnte, was ein Geschenk ist, wenn Sie eine korrupte Nutzlast per Halbierung eingrenzen.

Ein Kostümwechsel, den Sie kennen sollten: Wenn das Dekodieren über den umhüllten Stream passiert (die wrap(InputStream)-Variante, weiter unten behandelt), tauchen dieselben Probleme als IOException mit 0x-Präfix statt dessen auf: Illegal base64 character 0x20 (aktuelle JDKs; der Stream-Dekodierer von JDK 8 druckt den Nachschlagewert, -1, statt des Bytes). Gleiches Problem, andere Exception, leicht andere Schreibweise. Und der tolerante MIME-Dekodierer beschwert sich natürlich über nichts davon: Er überspringt einfach. Das ist der Preis der toleranten Stimmung.

Die Padding-Regeln

Jeder Base64-String in der Wildnis macht eine stillschweigende Zusage über das Padding, und Javas Zusage ist ungewöhnlich freundlich. Die Javadoc des Dekodierers sagt es genau: Das Padding-Zeichen = "wird akzeptiert und als Ende der kodierten Byte-Daten interpretiert, ist aber nicht erforderlich". Eine letzte Einheit aus zwei oder drei Zeichen dekodiert so, als wäre sie gepaddet, und wenn Pads vorhanden sind, müssen sie in der exakt richtigen Menge vorhanden sein. Das Verhalten des aktuellen JDK an den klassischen Beispielen:

Eingabe Ergebnis
"" leeres Byte-Array, kein Fehler
"Zm8" "fo", das Padding ist einfach nicht da
"Zm8=" "fo", die kanonische Schreibweise
"Zm8==" IllegalArgumentException: incorrect ending byte at 4
"Zm9v=" IllegalArgumentException: wrong 4-byte ending unit
"Zg==" "f", ein Byte
"Z=" IllegalArgumentException: last unit does not have enough valid bits
"AA==" genau ein Byte, das NUL-Byte 0x00
"AAAA" drei NUL-Bytes

Lesen Sie diese Tabelle zweimal. Der leere String dekodiert zu nichts, während AA== zu einem einzelnen NUL-Byte dekodiert: In Base64 sind "nichts" und "eine Null" verschiedene Wesen, und beide sind völlig gültige Eingaben. Und Padding muss, wenn es vorhanden ist, exakt sein: Zm8= ist richtig, Zm8== ist falsch, Zm9v= ist falsch, und ein Pad in der Mitte des Strings ist falsch. Praktische Konsequenz für Ihre eigenen Protokolle: Wählen Sie eine Schreibweise (mit oder ohne Padding) und erzwingen Sie sie auf beiden Enden, denn ein Wert, der in zwei Schreibweisen ankommen kann, ist ein Wert, der an irgendeiner Stelle einen simplen Gleichheitscheck brechen kann.

base64url: Das Alphabet für URLs gebaut

Standard-Base64 endet sein Alphabet mit + und /, und genau das sind die zwei Zeichen, die sich in URLs nicht benehmen: Ein + in einem Query-String ist bereits ein Leerzeichen, bevor Java es je zu Gesicht bekommt, ein / ist ein Pfadtrenner, und ein baumelndes = verlangt die Prozent-Kodierung zu einem dreizeichenlangen Ungeheuer. RFC 4648, Abschnitt 5, zeichnet den Fix: das URL- und Dateinamen-sichere Alphabet, in dem + zu - wird, / zu _, und das =-Padding am Ende typischerweise wegfällt, wenn die Länge implizit bekannt ist. Der RFC ist unerbittlich beim Namen: Diese Kodierung "sollte nicht als dasselbe wie die base64-Kodierung betrachtet werden". Sie treffen sie als base64url, und dort leben JSON Web Tokens, OAuth-State-Parameter, API-Session-IDs und elfstellige Video-IDs.

Die berühmteste base64url-Nutzlast im Web ist der JWT, und ein Blick hinein ist drei Zeilen Arbeit. Token-Teile sind nach Konvention nicht gepaddet, und der URL-Dekodierer ist damit zufrieden, denn Padding wird akzeptiert, ist aber nicht erforderlich - erinnern Sie sich daran:

import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class JwtPeek {
  public static void main(String[] args) {
    String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
      + ".eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ"
      + ".SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
    String[] parts = token.split("\\.");
    byte[] header = Base64.getUrlDecoder().decode(parts[0]);
    byte[] payload = Base64.getUrlDecoder().decode(parts[1]);
    System.out.println(new String(header, StandardCharsets.UTF_8));
    // {"alg":"HS256","typ":"JWT"}
    System.out.println(new String(payload, StandardCharsets.UTF_8));
    // {"sub":"1234567890","name":"John Doe","iat":1516239022}
  }
}

Zwei ehrliche Warnhinweise leben hier. Erstens: Ein JWT zu dekodieren ist ein Hinschauen, kein Vertrauen: Der dritte Teil ist eine Signatur, und die zwei Teile, die Sie gerade gelesen haben, sind weder geheim noch authentifiziert. Einer Nutzlast zu vertrauen, bevor ihre Signatur verifiziert ist, ist der klassische JWT-Bug, und der Fix besteht darin, die Verifizierung einer JOSE-Bibliothek wie JJWT (0.13.0) oder nimbus-jose-jwt (10.9.1) zu überlassen, statt eigenes Krypto zu rollen. Zweitens: Die Fehler benennen die Richtung. Geben Sie einen String im Standard-Alphabet an getUrlDecoder(), und Sie bekommen Illegal base64 character 2b oder 2f, und umgekehrt bringt Ihnen das 2d oder 5f. Alphabet-Mismatch ist der mit Abstand häufigste Base64-Dekodierfehler in der Wildnis, und die Fehlermeldung zeigt ihn binnen eines Herzschlags an. Wenn ein Token in einer Query-String eigentlich Standard-Base64 war, wurden dessen + und / wahrscheinlich vom Transport zerrissen, bevor sie Sie je erreichten, und der Dekodierfehler berichtet Ihnen von einem Bug upstream, nicht in Ihrem Dekodierer.

Von Bytes zu Wörtern

Jeder Decode-Aufruf in diesem Artikel hält bewusst bei den Bytes an, denn Base64 ist ein Byte-Format, Punkt. Die Frage "Was war das für ein Text?" ist eine, die Sie zu beantworten haben, und die moderne Standardantwort ist UTF-8. Es gibt allerdings ein Zeichensatz-Detail auf der Dekodier-Seite der API, das Menschen überrascht, also hier ist es. Das decode(String)-Overload interpretiert Ihren String nicht als UTF-8. Die Javadoc sagt es genau: Ein Aufruf "hat genau denselben Effekt wie der Aufruf von decode(src.getBytes(StandardCharsets.ISO_8859_1))". Das ist kein Bug - es ist ein Trick: Das Base64-Alphabet ist reines ASCII, also übergibt das Abbilden des Strings durch Latin-1 dem Dekodierer exakt dieselben Bytes bei null Umwandlungskosten, und jedes Nicht-ASCII-Zeichen in der Eingabe wird einfach zu einem ungültigen Symbol, das der strenge Dekodierer ablehnt (daher kommen die negativen Hexzahlen in den Fehlermeldungen).

Der Zeichensatz der Nutzlast ist eine komplett separate Entscheidung, die an der new String(bytes, charset)-Stelle fällt. Hier ist der klassische Fall: "café" in UTF-8 sind die fünf Bytes 63 61 66 C3 A9, die zu Y2Fmw6k= kodiert werden:

import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class CharsetDecode {
  public static void main(String[] args) {
    byte[] packed = Base64.getDecoder().decode("Y2Fmw6k=");
    System.out.println(new String(packed, StandardCharsets.UTF_8));
    // café, der Akzent überlebt
    System.out.println(new String(packed, StandardCharsets.ISO_8859_1));
    // caf gefolgt von Mojibake, die UTF-8-Bytes missverstanden als Latin-1
  }
}

Diese zweite Zeile ist der Fehlermodus, den Sie sofort erkennen sollten: eine UTF-8-Nutzlast, die durch Latin-1 gelesen wird, und das erzeugt einen String, der genau ein Zeichen zu lang ist und ein Byte danebenliegt. Das Heilmittel ist immer, sich mit dem Erzeuger auf einen Zeichensatz zu einigen und ihn explizit zu übergeben. Und zwar explizit im Code zu übergeben, nicht nur im Kopf: Der new String(bytes)-Konstruktor ohne Argumente verwendet den Standard-Zeichensatz der Plattform, der auf einem Windows-Server Cp1252 sein kann und auf einem älteren Linux alles Mögliche, worauf die Maschine gerade Lust hat. Seit JDK 18 (JEP 400, "UTF-8 by Default") ist der Standard auf jeder Plattform UTF-8, also ist die Argument-lose Form auf einer modernen JVM zufällig richtig, aber Ihr Code sollte es trotzdem sagen, denn der nächste, der ihn liest, sollte nicht wissen müssen, was der Standard ist. Und wenn die Nutzlast gar kein Text ist, bekommt derselbe Code nur ein anderes Ende: Bytes rein, Bytes raus, bis zum allerletzten Schritt.

Wenn die Nutzlast eine Datei ist

Die häufigste Datei-Aufgabe ist die Umkehrung irgendeiner Export-Routine: Eine .b64-Textdatei kommt an, und Sie brauchen die ursprüngliche Datei zurück. Mit strengem Dekodieren ist das schon produktionsreif geformt:

import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
public class DecodeFile {
  public static void main(String[] args) throws Exception {
    byte[] packed = Files.readAllBytes(Paths.get("payload.bin.b64"));
    byte[] raw = Base64.getDecoder().decode(packed);
    Files.write(Paths.get("payload.bin"), raw);
  }
}

Nichts auf diesem Weg schert sich, ob die Nutzlast eine Textdatei, ein ZIP-Archiv oder ein Video ist: byte[] sind einfach Bytes. Die Größen-Mathematik spielt auch Ihnen in die Hände: Die dekodierte Ausgabe ist drei Viertel der Länge der kodierten Eingabe, also verschlechtert Dekodieren den Speicher nie, und eine kodierte Datei von mehreren hundert Megabyte ist die kleinere von beiden. Eine gute Gewohnheit ist es, die Bytes sich selbst ankündigen zu lassen, bevor Sie irgendeinem Label glauben. Die ersten acht Bytes eines PNG sind immer die Magische Zahl 89 50 4E 47 0D 0A 1A 0A, was bedeutet, dass jedes Base64-kodierte PNG, das Sie je treffen, mit demselben Präfix beginnt, iVBORw0K: Wenn eine Nutzlast "behauptet", ein Bild zu sein, und so nicht anfängt, ist schon etwas schief.

Wenn Sie bereits den Ziel-Puffer besitzen, schreibt das Overload mit zwei Arrays direkt hinein und gibt genau zurück, wie viele Bytes gelandet sind, ohne jede Zwischenzuweisung:

import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class DecodeInto {
  public static void main(String[] args) {
    byte[] src = "SGVsbG8sIHdvcmxkIQ==".getBytes(StandardCharsets.ISO_8859_1);
    byte[] dst = new byte[16];
    int written = Base64.getDecoder().decode(src, dst);
    System.out.println(written); // 13
    System.out.println(new String(dst, 0, written, StandardCharsets.UTF_8));
    // Hello, world!
  }
}

Eine scharfe Kante bei diesem Overload, dokumentiert in der Javadoc: Wenn das Ziel zu klein ist, werden keine Bytes überhaupt geschrieben, und Sie bekommen IllegalArgumentException: Output byte array is too small for decoding all input bytes. Dimensionieren Sie den Puffer aus der simplen Mathematik, grob 3 * n / 4 minus das Padding, und die Exception zeigt nie ihr Gesicht. Es gibt auch ein ByteBuffer-Overload, das einen frischen Puffer zurückgibt, dessen Limit auf die dekodierte Länge gesetzt ist, praktisch, wenn Ihre Pipeline in NIO lebt.

Vom Draht: Header, JSON und Data URIs

Base64 trifft Java am häufigsten an der Netzwerkkante. Drei Formen verdienen jeweils ein durchgerechnetes Beispiel.

Form eins: der HTTP-Basic-Auth-Header. Der älteste Authentifizierungs-Header im Web reitet immer noch auf Base64. Nach RFC 7617 sendet eine Basic-Anforderung Authorization: Basic gefolgt von der Base64-Kodierung von username:password, und der RFC ist explizit, dass das Kodierung und kein Schutz ist: Jeder mit einem Packet-Capture kann beide Hälften mit einem Tastendruck lesen. Das eigene Beispiel des RFC, QWxhZGRpbjpvcGVuIHNlc2FtZQ==, dekodiert zu Aladdin:open sesame. Den Header auf der Server-Seite zu parsen ist ein paar Zeilen Arbeit:

import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class BasicAuth {
  public static String[] credentials(String header) {
    if (header == null || !header.startsWith("Basic ")) {
      return null;
    }
    byte[] packed = header.substring(6).getBytes(StandardCharsets.ISO_8859_1);
    byte[] raw = Base64.getDecoder().decode(packed);
    String userPass = new String(raw, StandardCharsets.UTF_8);
    int colon = userPass.indexOf(':');
    if (colon < 0) {
      return null;
    }
    return new String[] {userPass.substring(0, colon), userPass.substring(colon + 1)};
  }
}

Zwei Details halten das sicher. Die Aufteilung am ersten Doppelpunkt ist wichtig, denn ein Passwort darf legal seine eigenen Doppelpunkte enthalten. Und der Vergleich des dekodierten Passworts mit Ihrem gespeicherten Wert sollte in konstanter Zeit laufen: Hashen Sie beide Werte mit SHA-256 und vergleichen Sie die Digests mit MessageDigest.isEqual, niemals ein schlichtes equals, das ein Angreifer per Timing-Attacke in eine Benutzerliste verwandeln kann. Bieten Sie das nur über HTTPS an; auf einer unverschlüsselten Verbindung ist die Base64-Ebene nur Fensterputz.

Form zwei: Binär in JSON. Ein großer Teil moderner APIs bettet Binärdaten als Base64-Text in JSON ein: Datei-Upload-Endpunkte, Content-APIs, Secret-Stores und Webhooks tun alle das, denn rohe Bytes würden andernfalls die Escaping-Regeln des JSON-Strings brechen. Das Muster ist immer dasselbe: Das Feld kommt als schlichter String an, und Sie dekodieren es an der Grenze, nicht in Ihren Domänen-Objekten:

import java.util.Base64;
public class ApiField {
  public static void main(String[] args) {
    // Geparstes JSON trug:  "content" : "iVBORw0KGgoAAA..."
    String field = "iVBORw0KGgo=";
    byte[] image = Base64.getUrlDecoder().decode(field);
    // Manche APIs sprechen stattdessen Standard-Base64. Lesen Sie die Spezifikation,
    // und wählen Sie dann getDecoder() oder getUrlDecoder() entsprechend.
    System.out.println(image.length); // 8
  }
}

Die Falle hier ist nicht das Dekodieren; es ist das Lesen der Spezifikation. Manche APIs wollen Standard-Base64 mit Padding, manche base64url ohne, und ein paar sind bei beiden tolerant. Wenn die Spezifikation schweigt, ist der billigste Fix, sich einen Beispielwert von der anderen Seite anzusehen: Ein - oder _ irgendwo im Wert legt das Alphabet fest, und ein abschließendes = legt das Padding fest.

Form drei: die Data URI. Jemand fügt ein Bild in ein Formular ein, und das Frontend reicht Ihnen die komplette Data URI: data:image/png;base64,iVBORw0KGgo.... RFC 2397 definiert die Form: data:, ein optionaler Medientyp, eine optionale ;base64-Flagge, ein Komma und dann die Daten. Wenn die Flagge vorhanden ist, ist die Nutzlast Base64; wenn sie fehlt, ist die Nutzlast prozentkodierter Klartext, seltener, aber legal. Wenn der Medientyp weggelassen wird, ist der Standard text/plain;charset=US-ASCII. Eine aufzuteilen ist einfach:

import java.util.Base64;
public class DataUri {
  public static void main(String[] args) {
    String uri = "data:image/png;base64,iVBORw0KGgo=";
    int comma = uri.indexOf(',');
    String meta = uri.substring(5, comma);
    String payload = uri.substring(comma + 1);
    boolean isBase64 = meta.endsWith(";base64");
    String mime = isBase64 ? meta.substring(0, meta.length() - 7) : meta;
    byte[] raw = Base64.getDecoder().decode(payload);
    System.out.println(mime + " -> " + raw.length + " bytes");
    // image/png -> 8 bytes
  }
}

Zwei Fallen leben in diesem Format. Die fehlende ;base64-Flagge ist die erste: Eine legale Data URI ohne Flagge trägt eine prozentkodierte Nutzlast, und sie durch Base64.getDecoder() zu jagen wirft. Die zweite ist der behauptete Medientyp: Er ist ein Hinweis vom Sender, keine Tatsache, also prüfen Sie die Magischen Bytes dessen, was Sie dekodiert haben, bevor Sie es unter "png" ablegen. Und denken Sie an den eigenen Rat des RFC, dass Data URIs für kurze Werte sind; ein mehr-Megabyte-Bild in einer URL ist ein schlechtes Zeichen, kein Muster.

E-Mail, MIME und PEM-Armor

Base64 wurde für E-Mail geboren, und E-Mail-geformtes Base64 kommt immer noch ständig in Java-Programmen an. Der MIME-Standard (RFC 2045) machte Base64 zu einer der Binär-Transfer-Kodierungen und fügte zwei Hausregeln hinzu: kodierte Zeilen dürfen nicht länger als 76 Zeichen sein, und Dekodierer müssen jedes Zeichen außerhalb des Alphabets ignorieren, Zeilenumbrüche eingeschlossen. Die strengen Dekodierer lehnen bereits den allerersten Zeilenumbruch ab; getMimeDecoder() wurde für genau diese Eingabe gebaut:

import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class MimeDecode {
  public static void main(String[] args) {
    String wrapped = "SGVs\nbG8s\r\nIHN0\nYW5kYXJk";
    byte[] bytes = Base64.getMimeDecoder().decode(wrapped);
    System.out.println(new String(bytes, StandardCharsets.UTF_8));
    // Hello, standard
  }
}

Das funktioniert, und Sie sollten trotzdem den Haken kennen, denn der Haken hat Zähne. Der tolerante Dekodierer ignoriert nicht nur "Zeilenumbrüche"; er ignoriert alles, was nicht in seinem Alphabet steht. Wenn ein Standard-Base64-String durch eingeschlichene Zeichen korrupt wird, verschwindet der Müll, und der Rest dekodiert zu etwas Plausibel-Falschem, also greifen Sie nur nach getMimeDecoder(), wenn Sie tatsächlich MIME-geformte Eingabe erwarten.

MIMEs wildes Geschwister ist das PEM-Armor, das -----BEGIN CERTIFICATE------Geschäft, das Zertifikate und Schlüssel umhüllt. Hier ist die Falle: Die Armor-Zeilen stecken voller gewöhnlicher Alphabetzeichen. Die Buchstaben in "BEGIN CERTIFICATE" sind einfach Base64-Buchstaben, also dekodiert das Zufüttern eines ganzen PEM-Blocks, Armor inklusive, die Armor so, als wäre sie Daten. Entfernen Sie die Armor selbst und übergeben Sie dann den nackten Körper an einen Dekodierer:

import java.util.Base64;
public class PemDecode {
  public static void main(String[] args) {
    String pem = "-----BEGIN CERTIFICATE-----\n"
      + "TUlJQm96Q0NBVWlnQXdJQkFnSUpBSXBhVDJUaVFvZU1BMEdDU3FHU0liM0RRRUE9\n"
      + "-----END CERTIFICATE-----\n";
    String body = pem.replaceAll("(?m)^-----.*$", "").replaceAll("\\s", "");
    byte[] der = Base64.getDecoder().decode(body);
    System.out.println(der.length); // der DER-Body, Rüstung ausgeschlossen
  }
}

PEM bricht nach Konvention bei 64 Zeichen pro Zeile um (MIME bei 76), und wenn das Leerzeichen weg ist, kommen der strenge Dekodierer und der MIME-Dekodierer auf dasselbe Ergebnis. Verwenden Sie den strengen: Eine Überraschung hat wenigstens den Anstand, zu werfen. Für den dreckigen, aber Standard-Fall ist das klassische Rezept, die bekannten Leerzeichen zu entfernen und mit der strengen Instanz zu dekodieren, und jeden verbliebenen Müll mit einer IllegalArgumentException bestrafen zu lassen, statt ein korruptes Zertifikat hinzunehmen.

Konfigurations-, Umgebungs- und Datenbankwerte

Base64 ist ein Text-Container, deshalb taucht es an Orten auf, die Sie nicht erwarten. In Datenbanken kann ein binärer Blob (eine Datei, ein Icon, eine serialisierte Struktur) als Base64 in einer TEXT-Spalte leben und jedes Tool überleben, das Text annimmt; erwarten Sie, dass der gespeicherte Wert etwa ein Drittel größer als das Original ist, und dimensionieren Sie die Spalte entsprechend. In Konfigurationsdateien und Umgebungsvariablen ist Base64 der Trick, Werte zu schmuggeln, die das Format andernfalls brechen würden: eine DSN mit Semikolon, ein Passwort mit Anführungszeichen, ein mehrzeiliges Zertifikat. Dekodieren beim Start ist der ganze Job:

import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class ConfigDecode {
  public static void main(String[] args) {
    String value = System.getenv("DB_DSN_B64");
    if (value == null) {
      return;
    }
    byte[] raw = Base64.getDecoder().decode(value);
    String dsn = new String(raw, StandardCharsets.UTF_8);
    // dsn könnte sein: pg:host=db;password=qu"ote
  }
}

Dieselbe Vorsicht gilt hier doppelt. Erstens: Das ist Format-Sicherheit, nicht Geheimhaltung: Im Moment, in dem ein Entwickler die Konfigurationsdatei liest, kann er den Wert mit einem Aufruf dekodieren, also speichern Sie niemals ein Geheimnis als Base64 und nennen Sie es verschlüsselt; die Sicherheits-Sektion unten geht ins Detail. Zweitens: Prüfen Sie beim Start: Ein korrupter oder halb hineingeklebter Umgebungs-Wert ist eine IllegalArgumentException vom strengen Aufruf, und ein zweizeiliger Check verwandelt einen kryptischen Laufzeitfehler in eine handlungsfähige Startmeldung. Eine Java-spezifische Notiz für die Datenbank-Gemeinde: Halten Sie das Dekodierte als byte[] (ein byte[]-Parameter in Ihrem JDBC-Code), und führen Sie Binär niemals über einen String im Roundtrip zurück, denn die String-Konstruktoren sind der Ort, an dem binäre Nutzlasten sterben gehen.

Das Große streamen

Für Nutzlasten, die groß sind, aber noch in einen Puffer passen, den Sie verwalten, sind die Array-APIs in Ordnung. Für Nutzlasten, die gar nicht in den Speicher passen sollen, ist der Stream-Adapter der richtige Zug: wrap(InputStream) liefert einen Eingabe-Stream, der beim Lesen dekodiert, also muss eine kodierte Datei von mehreren Gigabyte nie in einem Byte-Array sitzen:

import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
public class StreamDecode {
  public static void main(String[] args) throws Exception {
    InputStream packed = Base64.getDecoder().wrap(Files.newInputStream(Paths.get("bigfile.b64")));
    OutputStream raw = Files.newOutputStream(Paths.get("bigfile.bin"));
    byte[] buf = new byte[8192];
    int n;
    while ((n = packed.read(buf)) != -1) {
      raw.write(buf, 0, n);
    }
    raw.close();
    packed.close();
  }
}

Zwei Details lohnen es zu wissen. Die read-Methoden des umhüllten Streams werfen eine IOException, wenn sie auf Bytes treffen, die nicht dekodiert werden können, also scheitert eine korrupte Datei mit einer Stream-Exception statt einer IllegalArgumentException. Und das Schließen des umhüllten Streams schließt den darunterliegenden Stream, deshalb schließt das Beispiel packed zuletzt, nach der Kopier-Schleife, und in Produktion würden Sie beide in einen try-with-resources-Block packen. (Der 8192-Puffer ist nur ein geräumiger Lesepuffer; der umhüllte Stream dekodiert intern, also ist die Größe, mit der Sie lesen, eine Performance-Entscheidung, keine Protokoll-Vorgabe.)

Jetzt eine Legacy-Warnung, denn das hier ist der eine echte Bug in der ganzen Geschichte, und er hat eine Bug-Nummer. Auf jedem JDK vor 16 (der Bug-Report reproduziert ihn auf 8, 10 und 11) hängt das Lesen eines umhüllten Dekodierers mit bestimmten Puffergrößen zwei verirte Null-Bytes ans Ende der dekodierten Daten: JDK 8222187, dessen klassische Reproduktion einen sieben-Byte-Lesepuffer mit einer einfachen acht-Byte-Eingabe paart, und er ist in JDK 16 gefixt. Wenn Sie auf einem Legacy-JDK 8 streamen müssen, prüfen Sie die dekodierte Länge nach der Kopie nach, denn der Bug feuert für bestimmte Eingabe-und-Puffer-Kombinationen, und selbst ein 4096-Byte-Puffer wurde in der Wildnis gemeldet, oder besser noch: Aktualisieren Sie das JDK; das würde ohnehin etwa hundert andere Dinge fixen.

Verpackungsklebeband, kein Schloss

Jetzt die Sektion, die die Sorgfältigen von den Verbrannten trennt. Base64 ist keine Verschlüsselung, und der Standard selbst bestätigt es gleich zweimal. RFC 4648, Abschnitt 12: Base-Kodierung "verdeckt optisch ansonsten leicht erkennbare Informationen, wie Passwörter, bietet aber keine rechnerische Vertraulichkeit", und er fährt fort zu bemerken, dass dies "bereits Sicherheitsvorfälle verursacht hat", wenn jemand einen Protokoll-Austausch in ein Ticket klebt und versehentlich das Passwort preisgibt. Auch der Rat des RFC an Implementierer verdient einen Rahmen: "Ein Dekodierer sollte bei ungültiger Eingabe einschließlich z. B. eingebetteter NUL-Zeichen nicht abbrechen".

Die subtilere Falle ist die Formbarkeit. Denken Sie daran, dass jedes Symbol sechs Bits trägt, und dass eine kurze letzte Einheit freie Bits übrig lässt, die in einer wohlgeformten Kodierung null sein müssen. Ein nachlässiger oder feindlicher Kodierer kann in diese freien Bits Müll legen, und das Ergebnis sieht immer noch völlig gültig aus: MQ== und MT== dekodieren beide zu dem einzelnen Byte der Ziffer 1. Java nimmt die verzeihende Seite davon ein: Base64.getDecoder().decode("MT==") prüft die nicht-signifikanten Bits nicht und reicht Ihnen frohgemut dasselbe Byte. Weshalb kümmern? Weil zwei verschiedene Strings, die zu denselben Daten dekodieren, die "einzigartige Schreibweise"-Annahme brechen, auf die Hash-Checks, Deduplizierung und Signatur-Vergleiche still vertrauen, und ein Angreifer, der einen kodierten Wert unterwegs manipulieren kann, kann eine Schreibweise durch die andere tauschen. Die 2022er Arbeit "Base64 Malleability in Practice" von Chatzigiannis und Chalkias (ACM ASIA CCS 2022) geht genau diesen Inkonsistenzen in realen Implementierungen nach. Die eigenen Worte des RFC über die freien Bits: Sie "können missbraucht werden, um Informationen zu entlocken, oder eingesetzt werden, um Zeichenketten-Gleichheitsvergleiche zu umgehen oder Implementierungsprobleme auszulösen". Die praktische Regel ist nicht "nie dekodieren"; sie ist "kenne deine Grenze": Für Daten zwischen Ihren eigenen Systemen ist die Großzügigkeit des JDK in Ordnung, aber für Daten, die eine Vertrauensgrenze überqueren, erzwingen Sie die kanonische Form (korrekte Länge, null freie Bits, eine Schreibweise des Paddings), bevor Sie irgendetwas davon glauben, was Sie dekodiert haben.

Performance-Notizen

Hier ist die gute Nachricht in einem Satz: Auf einer modernen JVM ist der eingebaute Dekodierer schnell genug, dass Base64 fast nie Ihre Engstelle ist, und er ist die Referenz, an der sich der Rest des Ökosystems benchmarkt. Ein Fall, der es auf den Punkt bringt: 2025 hat das gRPC-java-Projekt öffentlich sein auf Guava basierendes Base64-Handling gegen java.util.Base64 gemessen (Issue 11857), und die JDK-Implementation kam auf JDK 17 und 21 mit etwa 2,5- bis 3,8-mal schneller bei der Kodierung und 1,3- bis 2,1-mal schneller bei der Dekodierung heraus, wobei die größten Abstände auf x86 lagen. Das ist ein starker Hinweis darauf, wo der Implementation-Aufwand des JDK hingegangen ist, und es ist derselbe Schluss, den Sie in Base64-Benchmarks immer wieder finden: Die Standardbibliotheks-Version ist jetzt die schnelle, nicht die Legacy.

Zwei praktische Notizen. Erstens: Bei riesigen Dateien ist das Speicherprofil, nicht die Geschwindigkeit, was Sie verwalten, deshalb existiert die Streaming-Sektion: wrap(InputStream) hält den Arbeitssatz auf Ihrem Lesepuffer. Zweitens: Wenn Sie tatsächlich auf einem heißen Pfad landen, der Millionen kleiner Werte dekodiert, teilen Sie eine Dekodierer-Instanz (die Fabrik gibt bereits dieselbe gemeinsame zurück, wie oben angemerkt), überspringen Sie das decode(String)-Overload, wenn Sie bereits Bytes haben (es kopiert den String zuerst durch Latin-1), und lassen Sie das decode(byte[], byte[])-Overload in ein vorab dimensioniertes Ziel-Array schreiben, um den Tanz der Zuweisung zu überspringen.

Fallen mit Java-Akzent

Die Fallen an einem Ort gesammelt, alle Java-spezifisch:

  • Falscher Dekodierer für das Alphabet. Ein base64url-String in getDecoder() (oder umgekehrt) ist der klassische Illegal base64 character-Crash, normalerweise mit einem 2d, 5f, 2b oder 2f in der Meldung. Passen Sie den Dekodierer jedes Mal an das Protokoll an.
  • Abschließende Leerzeichen aus der Wildnis. Werte, die aus einem Terminal, einer Umgebungsvariablen oder einer Konfigurationsdatei kopiert wurden, kommen oft mit einem Zeilenumbruch an, und der strenge Dekodierer macht daraus Illegal base64 character a. Rufen Sie strip() auf der Eingabe auf, oder verwenden Sie den MIME-Dekodierer nur, wenn die Daten wirklich umhüllt sind.
  • Die Armor-Falle. getMimeDecoder() versteht keine PEM-Header, und die Buchstaben in BEGIN und CERTIFICATE dekodieren als Daten. Entfernen Sie die Armor-Zeilen selbst, immer.
  • MIME-Toleranz als Abkürzung. Dekodieren mit dem MIME-Dekodierer nur um "sicherzugehen" überspringt still jedes eingeschlichene Nicht-Alphabet-Zeichen, also kann eine korrupte Nutzlast plausibel und falsch herauskommen. Verwenden Sie ihn nur für echte MIME-Eingabe.
  • Zeichensatz dem Glück überlassen. Das new String(bytes) ohne Argumente verwendet den Plattform-Standard. Auf JDK 18+ ist er UTF-8, aber Ihr Code sollte StandardCharsets.UTF_8 explizit übergeben, sonst genießen Sie Mojibake nach der nächsten Server-Migration.
  • Binär stringifizieren. new String(decodedPng) und zurück ist Datenzerstörung: Jede Byte-Folge, die in Ihrem Zeichensatz nicht gültig ist, wird zum Ersetzungszeichen, und der Roundtrip ist einwegig. Bytes rein, Bytes raus, bis zum allerletzten Schritt.
  • Vertrauen in die freien Bits. MT== dekodiert genauso wie MQ==, also besteht eine Nutzlast mit in den nicht-signifikanten Bits verstecktem Müll jeden Check, den das JDK durchführt. Wenn das Protokoll zählt, erzwingen Sie die kanonische Form.
  • Streams in JDK 8, 11 und 12. Der umhüllte Dekodierer in diesen Versionen kann bei bestimmten Puffergrößen zwei verirte Null-Bytes anhängen (JDK 8222187, in 16 gefixt). Ab 16 ist das kein Problem; auf den älteren Versionen schon.
  • null ist nicht leer. null an decode() zu übergeben ist eine NullPointerException, kein leeres Array. Wenn eine Variable null sein kann, verpassen Sie ihr vor dem Aufruf einen Nicht-Null-Wert.
  • Android ist ein anderer Zoo. Auf Android existiert java.util.Base64 erst ab API-Level 26; darunter ist die Framework-Klasse android.util.Base64 mit ihren eigenen Flaggen-Konstanten. Code, der das eine oder das andere ohne Check hartkodiert, bricht genau auf den Geräten, die Sie nie getestet haben.
  • Vergessen, dass es keine Sicherheit ist. Base64 versteckt ein Passwort vor einem Blick und vor niemandem sonst. Wenn die Daten geheim sind, verschlüsseln Sie sie zuerst und packen Sie sie erst dann, wenn der Kanal Text verlangt.

Der lange Weg zu java.util.Base64

Die Format-Geschichte ist älter als Java. In den 1980ern konnte die E-Mail-Infrastruktur des Internets nur 7-Bit-ASCII tragen, und Leute, die Binärdaten bewegen wollten, erfanden lokale Dialekte: uuencode für UNIX (sein Alphabet läuft durch fortlaufende ASCII-Codes, also war Kodieren eine einzige Addition von 32 ohne Nachschlagetabelle) und BinHex für Apple-Maschinen (der sein Alphabet kuratiert hat, um optisch verwechselbare Zeichen wie 7, O, g und o zu streichen). 1987 standardisierte das Privacy-Enhanced-Mail-Protokoll (RFC 989) das 64-Zeichen-Schema mit 64-Zeichen-Zeilen für den Transport von Zertifikaten, und RFC 1421 im Jahr 1993 behielt das Alphabet und die Padding-Regeln bei. 1996 trug MIME (RFC 2045, die Aktualisierung von RFC 1521 aus 1993) das bereits nach seinem 64-Zeichen-Alphabet "base64" genannte Schema, setzte die 76-Zeichen-Zeilenlänge, die Ihre E-Mail-Anhänge bis heute umbrechen lässt, und schrieb die Regel des toleranten Dekodierers, die getMimeDecoder() bis heute implementiert. 2003 versuchte RFC 3548, die ganze Familie aufzuräumen, und erklärte, dass Dekodierer Zeichen außerhalb des Alphabets ablehnen sollen, und 2006 wurde RFC 4648 der Standard, den alle zitieren, mit den Alphabet-Tabellen, der base64url-Variante in Abschnitt 5 und der Sicherheits-Sektion, die die letzte Sektion dieses Artikels ehrlich hält.

Das eigene Kapitel von Java ist ein kleines Stück dramatischer. Jahrelang war das einzige Base64 im JDK das interne Paar sun.misc.BASE64Encoder und sun.misc.BASE64Decoder, die Art von API, die heute kompiliert und ohne Deprecation-Warnung verschwindet, und wenn Sie Base64 in einer XML-Welt brauchten, gab es außerdem javax.xml.bind.DatatypeConverter aus JAXB. Alle anderen benutzten Apache Commons Codec oder Guava. Dann der 18. März 2014: Java 8 lieferte java.util.Base64, das RFC 4648 und RFC 2045 in einer Klasse implementiert, mit dem Fabrikmethoden-Muster, das Sie die ganze Zeit benutzt haben. Drei Jahre und ein halbes später, Java 9 (21. September 2017), entfernte das sun.misc-Paar endgültig, und der offizielle Migrations-Leitfaden ist dabei nicht zimperlich: "Besonders wurden sun.misc.BASE64Encoder und sun.misc.BASE64Decoder entfernt. Stattdessen die unterstützte java.util.Base64-Klasse verwenden, die in JDK 8 hinzugefügt wurde". Führen Sie jdeps auf altem Code aus, der sie noch referenziert, und das Tool markiert die Abhängigkeit als "JDK removed internal API". Java 11 setzte nach, indem es das JAXB-Modul und seinen DatatypeConverter mit ihm entfernte (JEP 320). Seit 1.8 hat sich die öffentliche API nicht in einer einzigen Methode geändert, und die Javadoc trägt immer noch ihr ursprüngliches Since: 1.8-Tag. Bewegt hat sich der Motor darunter: Bugfixes (der JDK-8222187-Stream-Bug, in JDK 16 gefixt) und Performance-Arbeit, deshalb landen Community-Benchmarks immer wieder auf demselben Schluss. Zwölf Jahre, eine API, und es ist immer noch das schnellste Base64, für das Sie nichts bezahlen müssen.

Fun-Fakten, Java-Ausgabe

Weil ein vollständiger Leitfaden mit einem Lächeln enden sollte, hier ein paar Java-spezifische Fakten, die einfach Spaß machen:

  • Die Oracle-Javadoc für decode(byte[] src, byte[] dst) verspricht, dass "möglicherweise einige Bytes in das Ausgabe-Byte-Array geschrieben wurden, bevor die IllegalargumentException geworfen wird". Nicht IllegalArgumentException, IllegalargumentException, mit kleinem a. Der Tippfehler steckt im echten JDK-Quellcode, und er ist seit 2014 dort. Dokumentation, die sich so fest an einen Tippfehler gebunden hat, ist seltener, als sie sein sollte.
  • Dekodieren Sie einen String, der é enthält, und die Fehlermeldung lautet Illegal base64 character -17: eine negative Hexzahl, denn das Zeichen wird zum Latin-1-Byte 0xE9, das als signiertes Java-Byte minus 23 ist, und das JDK druckt es in Basis 16. Ihr Fehler-Logger treibt für einen Moment signierte Arithmetik.
  • Base64.getDecoder() == Base64.getDecoder() ist wahr. Der Quellcode gibt bei jedem Aufruf eine gemeinsame statische Instanz zurück, also ist die "einen neuen holen"-API ein Kostüm für einen Singleton, und die Threadsicherheits-Versprechung ist nur eine Beschreibung davon, was die JVM ohnehin schon tut.
  • Füttern Sie dem URL-Dekodierer einen String aus vier Unterstrichen, "____", und er gibt drei Bytes pures 0xFF zurück. Der Unterstrich ist Alphabet-Wert 63, vier davon machen 24 Bits, und 24 Bits aus Einsen sind das Byte-Tripel FF FF FF. Nichts Illegales daran, und genau das ist der lustigste Teil.
  • AA== dekodiert zu einem einzelnen NUL-Byte, während der leere String zu nichts dekodiert. In Base64 sind "nichts" und "eine Null" verschiedene Wesen, und beide sind völlig gültige Eingaben.
  • Der kleine String TWFu, der zu Man dekodiert, ist der Lieblings-Smoke-Test des Ökosystems geworden: Er taucht im RFC auf, in Wikipedia, in Referenz-Manuals und in den meisten Base64-Tutorials auf Erden, also leistet jeder seitdem geschriebene Dekodierer dieselbe kleine Huldigung.
  • Jedes Base64-kodierte PNG, das Sie je dekodiert haben, beginnt mit iVBORw0K. Das ist die PNG-Magische Zahl verkleidet, und sie ist eines der am besten erkennbaren Acht-Zeichen-Präfixe im Internet.
  • Die URL-sichere Sektion von RFC 4648 ist der Ort, an dem der Name "base64url" geboren wird: Die Spezifikation sagt, die Kodierung "kann als base64url bezeichnet werden", und warnt, sie "sollte nicht als dasselbe wie die base64-Kodierung betrachtet werden". Der Ursprung des URL-sicheren Alphabets ist in einer Fußnote einem 2001er-Post auf einer P2P-hackers-Mailingliste zugeschrieben, also hat der Name, den Sie in jede URL kleben, eine Mailinglisten-Abstammung.
  • YouTube-Video-IDs sind base64url ohne Padding, der bekannte elfstellige String, den Sie überall dort einfügen können, wo eine URL erlaubt ist. Das Format, das für E-Mail-Anhänge designed wurde, betreibt jetzt eine Video-Plattform, und getUrlDecoder() ist der Teil Ihres JDK, der es laufen lässt.
  • Dekodieren Sie den String YmFzZTY0, und Sie bekommen das Wort base64 zurück, kein Padding nötig, denn sechs ist ein Vielfaches von drei. Ein Format, das sich selbst beschreibt, ist das technische Äquivalent eines Spiegels, der in Morse spricht.

Die andere Richtung

Das ist die Dekoder-Seite der Geschichte, und dort wohnt der größte Teil des Schmerzes, denn beim Dekodieren treffen Sie auf die Daten anderer Leute: ihre Padding-Entscheidungen, ihre Zeilenumbrüche, ihre Zeichensätze, ihre Tokens, ihre Armor. Die andere Richtung, Bytes mit den Kodierern von java.util.Base64 in einen Base64-String zu verwandeln, ist ein ruhigeres Tier: Es wirft nie auf ungültiger Eingabe (es gibt keine ungültige Eingabe zum Kodieren), es hat eine Größen-Rechnung zu begleichen, statt eine Fehlermeldung zu lesen, und seine eigene Reihe von Fallen (der Zeichensatz-Schritt, die MIME-Regler, die Padding-Entscheidung für Tokens) bekommt einen eigenen Leitfaden. Base64-Kodierung in Java, von dieser Seite aus verlinkt, behandelt den Kodierer in derselben Tiefe, und die beiden lassen sich gut als Paar lesen.

Zuletzt aktualisiert: 2026-09-08

Verwandter Artikel: Base64-Kodierung in Java: Ein vollständiger Leitfaden