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-Kodierung in Java: Ein vollständiger Leitfaden

Hier ist die Lage: Sie haben Bytes. Eine Datei, ein Passwort, ein Zertifikat, eine 13-Byte-Begrüßung, einen 200-Megabyte-Upload. Und Sie brauchen sie in etwas, das nur Text versteht: ein JSON-Feld, ein HTTP-Header, eine Datenbank-Spalte, eine URL, eine Konfigurationsdatei. Das ist der gesamte Job von Base64, und dieser Leitfaden ist das Java-Handbuch, um es gut zu machen. Kurze Orientierung, denn die Startseite geht das Format Schritt für Schritt durch: Base64 schreibt jeweils drei Bytes der Daten in vier Zeichen aus einem Alphabet von 64 Buchstaben um, mit ein oder zwei =-Pads, die angehängt werden, wenn das letzte Stück kurz ist. Der Preis der Fahrt ist die Größe: Alle drei Bytes werden zu vier Zeichen, also landet die kodierte Ausgabe etwa 33 Prozent größer als die Eingabe, plus noch ein wenig mehr, wenn Zeilenumbrüche ins Spiel kommen.

Die Schlagzeilen-Nachricht, und sie ist eine gute. Seit dem 18. März 2014 liefert jeder JDK ein vollständiges Base64-Werkzeugset in der Standardbibliothek mit: java.util.Base64. Kein Download, keine Maven-Koordinate, keine native Bibliothek. Ein Import, drei Kodierer-Persönlichkeiten, und dasselbe Verhalten von Java 8 bis zum heutigen Java 26. Alles in diesem Artikel baut auf genau dieser einen Klasse auf, und sie wirft nie auf den Daten selbst: Der Job des Kodierers kann an ungültiger Eingabe nicht scheitern, denn jedes mögliche Byte ist kodierbar.

Eine ehrliche Grenze, bevor wir loslegen: Dies ist die Kodierer-Seite der Geschichte. Sie lernen die String-nach-Bytes-Entscheidung, die tatsächlich die Korrektheit bestimmt, die Padding- und Umbruch-Regler, base64url und seinen Modus ohne Padding für Tokens, und die Anwendungsfälle, in denen Java-Entwickler am häufigsten auf kodierte Ausgabe treffen. Dekodieren, wo der größte Teil des echten Schmerzes wohnt, bekommt seinen eigenen Leitfaden und ist am Ende von diesem verlinkt.

Ein Import, null Downloads

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 zwölf Jahre später 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 einem Debian-basierten System ist das ein einziger Befehl:

sudo apt install openjdk-17-jdk-headless

Die API ist eine Fabrik: Sie konstruieren nie einen Kodierer; Sie bitten die Klasse darum. Die Kodierer-Seite hat vier Türen, die alle Instanzen der verschachtelten Klasse Base64.Encoder zurückgeben:

Factory-Methode Alphabet Ausgabeform
getEncoder() A-Z a-z 0-9 + / Gepaddet, keine Zeilenumbrüche
getUrlEncoder() A-Z a-z 0-9 - _ Gepaddet, keine Zeilenumbrüche
getMimeEncoder() A-Z a-z 0-9 + / Gepaddet, 76-Zeichen-Zeilen, CRLF
getMimeEncoder(int, byte[]) A-Z a-z 0-9 + / Gepaddet, Ihre Zeilenlänge, Ihr Trenner

Drei Eigenschaften lohnen es, sie vorab zu kennen. Die Instanzen sind thread-sicher, und die Fabrik gibt bei jedem Aufruf dieselbe geteilte Instanz zurück, also ist Base64.getEncoder() == Base64.getEncoder() wahr; legen Sie eine in einem statischen Feld an und teilen Sie sie überall. Die Kodierer werfen nie auf den Daten: Jeder Byte-Wert hat eine Kodierung, es gibt also keinen "ungültige Eingabe"-Zustand, den man handhaben müsste, und die einzigen Ausnahmen, die Sie treffen werden, drehen sich um Fehlkonfiguration (ein schlechter Zeilentrenner) oder ein zu kleines Ziel-Array. Und jeder Kodierer in dieser Liste fügt standardmäßig Padding hinzu; der Regler, der es ausschaltet, withoutPadding(), erscheint im base64url-Abschnitt, denn dort werden Sie ihn brauchen.

Sie werden in Codebasen immer noch ältere Bibliotheken treffen, also hier eine schnelle Landkarte. Apache Commons Codec (aktuell 1.22.1) liefert seit 1.0 sein eigenes org.apache.commons.codec.binary.Base64 mit, mit einer Builder-API, die die strikt-oder-nachsichtig-Regel, die Zeilenlänge und den Trenner als Regler offenlegt; sie ist das richtige Werkzeug nur, wenn Sie JVMs vor Java 8 unterstützen müssen. Guava liefert com.google.common.io.BaseEncoding, einen ebenso fähigen Veteranen, in Big-Data-Stacks immer noch verbreitet. Für alles auf einer modernen JVM ist java.util.Base64 der Standard: null Abhängigkeiten, und Community-Benchmarks finden immer wieder, dass es das schnellste der Truppe ist (mehr dazu im Abschnitt über Sicherheit und Geschwindigkeit).

Ihr erstes Kodieren

Neunzig Prozent des Kodier-Alltags passen in drei Zeilen. Hier ist die ganze Zeremonie, mit dem kleinsten Beispiel, das der Wikipedia-Artikel über Base64 verwendet, um das Alphabet zu erklären:

import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class FirstEncode {
  public static void main(String[] args) {
    byte[] text = "Man".getBytes(StandardCharsets.UTF_8);
    String packed = Base64.getEncoder().encodeToString(text);
    System.out.println(packed); // TWFu
  }
}

Der String TWFu ist das Beispiel, das der Wikipedia-Artikel über Base64 verwendet, um das Alphabet zu erklären, also ist die Maschine ehrlich, wenn Ihr Kodierer "Man" darin verwandelt. Aber schauen Sie sich die erste Zeile dieses Beispiels an, denn das ist die Zeile, in der in Java tatsächlich kodiert wird. Es gibt bewusst keine encodeToString(String)-Methode. Ein Java-String ist eine Folge von UTF-16-Codeeinheiten, nicht Bytes, und Base64 ist ein Byte-Format, also lässt die API die Byte-Frage an Ihnen selbst entscheiden: "Man".getBytes(StandardCharsets.UTF_8). Genau dieser eine Aufruf, mit einem expliziten Zeichensatz, ist der Ort, an dem "café" die nächsten hundert Jahre korrekt bleibt, und er ist die eine einzige wichtigste Angewohnheit in diesem ganzen Artikel. Der nächste Abschnitt ist ihm gewidmet, denn die Alternative ist der klassische Mojibake-Bug.

Zwei Notizen zur zweiten Zeile. encodeToString() gibt einen String zurück, der aus den kodierten Bytes aufgebaut ist; die Javadoc erklärt, dass sie das Ergebnis mit dem ISO-8859-1-Zeichensatz konstruiert, was in der Praxis keine Rolle spielt, denn jedes Base64-Ausgabezeichen ist schlichtes ASCII und sieht in Latin-1, UTF-8 und dem größten Teil des übrigen Zeichensatz-Zoos identisch aus. Und wenn Sie den Ausgabe-Buffer lieber selbst besitzen wollen, gibt encode(byte[]) einen frischen byte[] zurück, und encode(byte[] src, byte[] dst) schreibt in ein Ziel, das Sie liefern, und gibt die Anzahl zurück (und wirft IllegalArgumentException: Output byte array is too small for encoding all input bytes, wenn das Ziel zu kurz ist, ohne ein einziges Byte zu schreiben).

Die Zeichensatz-Entscheidung

Machen wir den String-nach-Bytes-Schritt am klassischen Fall konkret. Das Wort "café" ist ein Wort, aber in Bytes hängt es vollständig vom Zeichensatz ab, den Sie gewählt haben:

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class CharsetEncode {
  public static void main(String[] args) {
    byte[] utf8 = "café".getBytes(StandardCharsets.UTF_8);
    byte[] latin1 = "café".getBytes(Charset.forName("ISO-8859-1"));
    System.out.println(utf8.length + " vs " + latin1.length);
    // 5 vs 4: Der Akzent ist zwei Bytes in UTF-8, eines in Latin-1
    System.out.println(Base64.getEncoder().encodeToString(utf8));
    // Y2Fmw6k=
    System.out.println(Base64.getEncoder().encodeToString(latin1));
    // Y2Fm6Q==
  }
}

Zwei verschiedene Base64-Strings für ein Wort, und beide sind "korrekt", solange dem Leser gesagt wird, welchen Zeichensatz er verwenden soll. Die ganze Lektion passt in eine Zeile: Der Kodierer ist den Bytes, die Sie ihm geben, treu, und Sie sind für die Bytes verantwortlich. In der Praxis heißt das: einigen Sie sich mit Ihrem Gegenüber auf UTF-8, übergeben Sie StandardCharsets.UTF_8 explizit, und schreiben Sie den Zeichensatz in die Spezifikation, das Schema oder die Commit-Nachricht, denn niemand auf der empfangenden Seite kann ihn aus dem Base64 allein erraten. Der Zwilling dieses Bugs auf der Dekodierer-Seite ist das Thema des Schwester-Leitfadens.

Eine Versionsnotiz, denn sie verändert den Fehlermodus von faulem Code. Der argumentfreie new String(bytes) und das zeichensatzlose String.getBytes() verwenden den Standard-Zeichensatz der Plattform, der historisch auf Windows Cp1252 war und auf Linux irgendetwas Locale-Abhängiges. Seit JDK 18 (JEP 400, "UTF-8 als Standard") ist der Standard auf jeder Plattform UTF-8, also trifft die faule Form auf einer modernen JVM zufällig ins Schwarze. Das macht sie nicht sicher: Ihr Code überlebt das JDK, für das er geschrieben wurde, und die Person, die ihn erbt, sollte nicht wissen müssen, was der Standard ist. Schreiben Sie den Zeichensatz.

Ein verwandtes Design-Detail: Irgendwo in der API gibt es keinen encode(String)-Overload, und das ist beabsichtigt. Jeder andere Schritt der Pipeline (Arrays, Buffer, Streams) nimmt Bytes an, und eine String-akzeptierende Methode müsste für Sie einen Zeichensatz wählen, und genau das ist die Entscheidung, die der JDK sich verwehrt. Die eine Methode mit String-Typ, die existiert, encodeToString, liegt auf der Ausgabeseite, wo die Zeichensatz-Frage nicht existiert: Base64-Ausgabe ist reines ASCII. Die gesamte Form der API ist ein kleines Plädoyer für "entscheiden Sie Ihre Bytes mit Absicht".

Padding, Umbruch und der MIME-Regler

Javas Kodierer treffen standardmäßig zwei Formatierungs-Entscheidungen für Sie, und beide lohnen es, sie zu verstehen, denn beide sind Regler, die Sie drehen können. Der erste ist das Padding: Jeder Kodierer fügt die =-Zeichen hinzu, die die Ausgabe zu einem Vielfachen von vier machen, wie es RFC 4648 verlangt: Implementierungen MÜSSEN geeignete Füllzeichen am Ende der kodierten Daten einfügen, sofern die referenzierende Spezifikation nichts anderes sagt. Der zweite ist der Zeilenumbruch: Nur der MIME-Kodierer bricht um, bei 76 Zeichen mit Wagenrücklauf und Zeilenvorschub, und er fügt nach der letzten unvollständigen Zeile keinen Zeilentrenner hinzu, ein Detail, das die Javadoc ausdrücklich hervorhebt und andere Tools falsch machen:

Kodierer Padet die Ausgabe Bricht Zeilen um Zeilentrenner
getEncoder() ja nein k. A.
getUrlEncoder() ja nein k. A.
getMimeEncoder() ja ja, 76 Zeichen CRLF
getMimeEncoder(64, "\n") ja ja, 64 Zeichen LF

Der MIME-Regler ist der nützlichste Teil der API für Leute, die die Formate anderer Leute erben. Der Standard-Konstruktor ist getMimeEncoder() (76, CRLF, direkt aus RFC 2045); die Zwei-Argument-Version, getMimeEncoder(int lineLength, byte[] lineSeparator), lässt Sie andere Konventionen nachbauen. Die zwei Kuriositäten, die man kennen sollte: Die Zeilenlänge wird "abgerundet auf das nächstniedrigere Vielfache von 4", also gibt die Bitte um 77 still und leise 76, und ein abgerundeter Wert, der nicht positiv ist, gibt gar keinen Umbruch; und der Trenner darf kein Zeichen des Base64-Alphabets enthalten, sonst wirft der Konstruktor auf der Stelle eine IllegalArgumentException, denn ein Trenner, der mit Daten verwechselt werden könnte, ist ein vorprogrammierter Bug. Hier ist der Regler in Aktion, MIME-Standard und im PEM-Stil:

import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class WrapDials {
  public static void main(String[] args) {
    byte[] data = "Hello, wrapped world! This line keeps going and going and going until it finally has to wrap.".getBytes(StandardCharsets.UTF_8);
    Base64.Encoder mime = Base64.getMimeEncoder();
    Base64.Encoder pem = Base64.getMimeEncoder(64, "\n".getBytes(StandardCharsets.ISO_8859_1));
    System.out.println(mime.encodeToString(data));
    // 76-Zeichen-Zeilen, CRLF dazwischen
    System.out.println(pem.encodeToString(data));
    // 64-Zeichen-Zeilen, nacktes LF dazwischen
  }
}

Zwei praktische Notizen. Wenn Ihr Verbraucher erwartet, dass ein umgebrochener String mit einem Zeilenumbruch endet (einige E-Mail-Tools tun das), fügen Sie ihn selbst nach dem Kodieren hinzu: Der JDK hält absichtlich nach der letzten unvollständigen Zeile an. Und wenn Sie Daten erzeugen, die in einer URL oder einem Token leben werden, ist der Umbruch überhaupt der falsche Regler; diese Verbraucher wollen eine lange Zeile und normalerweise kein Padding, und das ist der nächste Abschnitt.

base64url und der Regler ohne Padding

Standard-Base64 endet im Alphabet mit + und /, und genau das sind die beiden Zeichen, die sich in URLs nicht benehmen: Ein + in einem Query-String ist noch bevor der Server es parst, bereits ein Leerzeichen, ein / ist ein Pfadtrenner, und ein anhängendes = will in ein dreizeichiges Monster prozent-kodiert werden. Abschnitt 5 von RFC 4648 zeichnet die Korrektur: das URL- und Dateinamen-sichere Alphabet, in dem + zu - wird, / zu _ wird und das anhängende =-Padding typischerweise fallen gelassen wird, wenn die Länge implizit bekannt ist. Der RFC ist beim Namen unerbittlich: Diese Kodierung "sollte nicht als dasselbe wie die base64-Kodierung betrachtet werden", und der Name, den Sie hören werden, ist base64url. JSON Web Tokens, OAuth-State-Parameter, API-Sitzungs-IDs und elfzeichenlange Video-IDs leben alle in diesem Dialekt.

Java gibt Ihnen das Alphabet mit getUrlEncoder(), aber hier ist der Regler, der einen erwischt: Der URL-sichere Kodierer padet standardmäßig noch, und die Token-Standards wollen kein Padding. RFC 7515 ist explizit, dass JWS-Teile base64url "mit allen anhängenden '='-Zeichen ausgelassen ... und ohne die Aufnahme von Zeilenumbrüchen, Leerraum oder anderen zusätzlichen Zeichen" verwenden. Das kanonische Java-JWT-Rezept ist also eine Zwei-Methoden-Kette:

import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class TokenParts {
  public static void main(String[] args) {
    Base64.Encoder url = Base64.getUrlEncoder().withoutPadding();
    byte[] header = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}".getBytes(StandardCharsets.UTF_8);
    byte[] payload = "{\"sub\":\"1234567890\",\"name\":\"John Doe\"}".getBytes(StandardCharsets.UTF_8);
    System.out.println(url.encodeToString(header));
    // eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
    System.out.println(url.encodeToString(payload));
    // eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0
  }
}

Der withoutPadding()-Aufruf gibt eine neue Kodierer-Instanz zurück, die sich identisch verhält, außer dass sie die anhängenden Pads weglässt; die Original-Instanz bleibt unangetastet, und die Javadoc sagt es präzise. Die Dekodierer-Seite akzeptiert sowohl gepaddete als auch ungepaddete Eingabe, also wird ein Wert, den Sie ohne Padding erzeugen, auch von einem strikten Dekodierer lesbar sein, und deshalb ist ungepaddet die sichere Wahl für alles, was eine API-Grenze überquert. Jetzt ein großer Disclaimer: Die beiden Teile oben sind die nicht signierten Hälften eines JWT. Ein echtes Token braucht eine Signatur, berechnet über "header.payload", und das ist Kryptografie, keine Kodierung. Für die Produktion prägen und verifizieren Sie Tokens mit einer JOSE-Bibliothek: JJWT (0.13.0) oder nimbus-jose-jwt (10.9.1). Das API-Artefakt von JJWT ist zum Beispiel nur eine Koordinate entfernt:

<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt-api</artifactId>
  <version>0.13.0</version>
</dependency>
<!-- jjwt-impl und jjwt-jackson zur Laufzeit hinzufügen, gemäß den Projekt-Doku -->

YouTube-IDs sind die andere Seite dieses Reglers: Elf Zeichen base64url ohne Padding, eine Kennung, die es überleben muss, überall in einer URL eingefügt zu werden. Wenn Ihr System Kennungen erzeugt, die in URLs reisen, ist die withoutPadding()-Kette oben die Form, die Sie kopieren.

Dateien kodieren

Der alltägliche Datei-Job ist der Spiegel des Lieblings des Dekodierers: eine Datei lesen, sie kodieren, den Text heraus schreiben. Mit java.nio.file vier Zeilen:

import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
public class EncodeFile {
  public static void main(String[] args) throws Exception {
    byte[] raw = Files.readAllBytes(Paths.get("report.pdf"));
    String packed = Base64.getEncoder().encodeToString(raw);
    Files.write(Paths.get("report.pdf.b64"), packed.getBytes(StandardCharsets.ISO_8859_1));
    System.out.println(raw.length + " -> " + packed.length());
  }
}

Dieser letzte Print oben macht die 33-Prozent-Rechnung sichtbar. Eine 1-MB-Datei wird etwa 1,33 MB Text (4/3 des Originals, plus höchstens zwei Füllzeichen), und wenn man sie MIME-artig umbricht, addieren die Zeilenumbrüche ein paar Prozent dazu: Die alte Mail-Ära-Rechnung, immer noch gültig, ist 4/3 mal 78/76, also etwa 1,37-mal das Original für einen umgebrochenen MIME-Payload. Zwei Konsequenzen. Erstens: Dimensionieren Sie Speicher- oder Nachrichtenfelder nach der kodierten Länge, nicht nach der rohen Länge: Eine VARCHAR(255)-Spalte, die einen 192-Byte-Rohwert gerne fasst, wird seine 256-Zeichen-Kodierung ablehnen. Zweitens: Die Kodierungs-Richtung ist die, die den Speicher verschlechtert, also ist für große Dateien die Array-Version das falsche Werkzeug und der Streaming-Abschnitt das richtige. Eine kleine Freude für die Datei-Leute: Weil die ersten Ausgabe-Zeichen eine reine Funktion der ersten Eingabe-Bytes sind, beginnt jedes Base64-kodierte PNG mit iVBORw0K und jedes kodierte GIF mit R0lGOD; Sie können den Dateityp erkennen, bevor auch nur ein Byte dekodiert ist.

JSON, APIs und Data URIs

Zwei der häufigsten Orte, an denen kodierte Ausgabe auf dem Draht lebt.

Eins: Binär in JSON. Datei-Upload-Endpunkte, Content-APIs, Secret-Stores und Webhooks betten Binäres als Base64-Text in JSON ein, denn rohe Bytes würden das JSON-String-Escaping brechen. Die Kodierer-Seite ist an der Grenze eine Zeile, und die eine Entscheidung ist, welchen Dialekt die Spezifikation verlangt:

import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
public class JsonField {
  public static void main(String[] args) throws Exception {
    byte[] image = Files.readAllBytes(Paths.get("logo.png"));
    // Die Spezifikation sagt base64url, ungepaddet:
    String field = Base64.getUrlEncoder().withoutPadding().encodeToString(image);
    // Geben Sie "field" Ihrer JSON-Bibliothek als einfachen String-Wert.
    System.out.println(field.length());
  }
}

Die Falle ist nicht das Kodieren; sie ist das Lesen der Spezifikation. Manche APIs wollen Standard-Base64 mit Padding, manche base64url ohne, und einige sind bei beiden nachsichtig. Wenn die Spezifikation schweigt, ist die billigste Lösung, einen Beispielwert von der anderen Seite anzuschauen: ein - oder _ irgendwo klärt das Alphabet, und anhängende = klären das Padding. Den Dialekt falsch zu wählen, lässt die andere Seite normalerweise nicht abstürzen; es beschädigt normalerweise die Datei, und das ist die langsamste Art von Bug, die man finden kann.

Zwei: Data URIs. Der data:image/png;base64,...-String, der ein Bild in HTML oder CSS einbettet, ist die Data URI von RFC 2397: data:, ein optionaler Medientyp, ein optionales ;base64-Flag, ein Komma, dann die Daten. Eine zu bauen ist String-Konkatenation, und die eine Entscheidung ist, ob das Flag da ist (kein Flag bedeutet, der Payload ist prozent-kodierter Text, was niemand für Binäres will):

import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
public class DataUriBuild {
  public static void main(String[] args) throws Exception {
    byte[] icon = Files.readAllBytes(Paths.get("icon.png"));
    String b64 = Base64.getEncoder().encodeToString(icon);
    String uri = "data:image/png;base64," + b64;
    System.out.println(uri.substring(0, Math.min(40, uri.length())) + "...");
    // data:image/png;base64,iVBORw0KGgo...
  }
}

Der eigene Rat des RFC gilt umso mehr: Data URIs sind für kurze Werte. Ein 50-KB-Icon einzubetten ist ein normaler Trade-off (eine Anfrage weniger); ein 5-MB-Foto einzubetten ist ein Performance-Bug im Komfort-Kostüm. Behalten Sie das Flag, halten Sie den Medientyp ehrlich, und halten Sie die Bytes klein.

Den Basic-Auth-Header bauen

Der älteste Authentifizierungs-Header des Webs ist immer noch der einfachste Base64-Use-Case in Java, denn er ist genau ein Kodier-Aufruf. Gemäß RFC 7617 schickt eine Basic-Anfrage Authorization: Basic gefolgt von der Base64-Kodierung von username:password, und das eigene Beispiel des RFC, QWxhZGRpbjpvcGVuIHNlc2FtZQ==, ist "Aladdin:open sesame" im Verkleidungskostüm. Auf der Client-Seite ist das Bauen des Headers zwei Zeilen Base64 plus ein moderner HTTP-Aufruf:

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class BasicAuthClient {
  public static void main(String[] args) throws Exception {
    byte[] credentials = ("alice:secret123").getBytes(StandardCharsets.UTF_8);
    String header = "Basic " + Base64.getEncoder().encodeToString(credentials);
    HttpClient client = HttpClient.newHttpClient();
    HttpRequest request = HttpRequest.newBuilder()
      .uri(URI.create("https://example.com/api/status"))
      .header("Authorization", header)
      .GET()
      .build();
    HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
    System.out.println(response.statusCode());
  }
}

Drei Vorsichtnoten gehören zu diesem Header. Erstens: Der RFC ist explizit, dass Basic Kodierung ist, kein Schutz: Die Credentials sind für jeden lesbar, der die Pakete sehen kann, also ist dieser Header nur so stark wie das HTTPS darunter, und er ist auf allem außer TLS eine schlechte Idee. Zweitens der Zeichensatz: Der RFC erwartet US-ASCII-Credentials (sonst UTF-8, und der charset-Auth-Parameter ist beratend), also wählen Sie StandardCharsets.UTF_8 und bleiben Sie auf beiden Seiten konsistent. Drittens eine Versionsnotiz: Der java.net.http-Client ist von Java 11; auf einer älteren JVM fährt derselbe Header auf einem HttpURLConnection mit einem setRequestProperty-Aufruf, und die Base64-Zeile ist in beiden Fällen identisch. Auf der Server-Seite desselben Headers sind das Parsen und Dekodieren das Beispiel aus dem Schwester-Leitfaden, mit der Aufteilung am ersten Doppelpunkt und dem konstantzeit-Vergleich. Die beiden Seiten sind zwei Aufrufe derselben API, und das ist die stille Eleganz dieses Ganzen.

Werte in Configs, Env-Vars und Spalten

Base64 ist ein Text-Container, deshalb taucht er an Orten auf, an die man nicht denkt: eine Datenbank-DSN mit Semikolons in einer Env-Datei, ein Passwort mit Anführungszeichen in einer Properties-Datei, ein mehrzeiliges Zertifikat in einem Config-Map, ein binärer Blob in einer TEXT-Spalte, weil das Schema vor der Zeit entworfen wurde, in der jemand an BLOBs dachte. Die Kodierer-Seite ist ein Aufruf, und die ehrliche Einordnung ist, was es ist: ein Format-Sicherheits-Trick, kein Geheimhaltung-Trick:

import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class ConfigEncode {
  public static void main(String[] args) {
    String dsn = "pg:host=db;password=qu\"ote";
    byte[] raw = dsn.getBytes(StandardCharsets.UTF_8);
    String packed = Base64.getEncoder().encodeToString(raw);
    System.out.println(packed);
    // cGc6aG9zdD1kYjtwYXNzd29yZD1xdSJvdGU=
    System.out.println("DB_DSN_B64=" + packed);
  }
}

Zwei Regeln halten das ehrlich. Erstens: Speichern Sie nie ein Geheimnis als Base64 und nennen Sie es verschlüsselt: Base64 fügt keine Entropie hinzu und entfernt keine Information; in dem Moment, in dem ein Entwickler die Datei liest, kann er den Wert in einem Aufruf dekodieren, und der Sicherheitsabschnitt des RFC zeigt auf genau dieses Scheitern, Leute, die Credentials verraten, indem sie "kodierte" Protokoll-Austausche einfügen. Wenn ein Wert ein Geheimnis ist, verschlüsseln Sie ihn zuerst, und verpacken Sie dann nur den Chiffretext in Base64, wenn der Kanal Text verlangt. Zweitens, kalkulieren Sie die Größe: Der gespeicherte Wert ist etwa ein Drittel größer als das Original, und eine Spalte oder ein Feld, das den rohen Wert fasste, fasst den kodierten nicht. Und wenn der Wert zurückkommt, dekodieren Sie ihn an der Grenze und halten Sie ihn als Bytes (für Binäres) oder einen String mit explizitem Zeichensatz (für Text); die Richtung ist das Territorium des Schwester-Leitfadens.

Streaming für große Daten

Kodieren ist die Richtung, die den Speicher verschlechtert, also dreht sich die große-Datei-Geschichte hier darum, die Arbeitsmenge klein zu halten. Die Array-Version des Beispiels aus dem Dateien-Abschnitt ist gut, bis die Datei aufhört, bequem in den Speicher zu passen; darüber hinaus ist der Stream-Adapter der Zug. wrap(OutputStream) gibt einen Ausgabestream zurück, der kodiert, während Sie schreiben, sodass eine Datei mit mehreren Gigabyte nie als ein einzelnes Byte-Array gehalten wird:

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

Es gibt ein Verhalten an diesem Stream, das eine Hervorhebung verdient, denn die Javadoc selbst zeigt darauf: Der verpackte Stream kann einige verbleibende Bytes intern halten, und die empfohlene Praxis ist, "den zurückgegebenen Ausgabestream nach Gebrauch umgehend zu schließen, wobei er alle möglichen verbleibenden Bytes in den zugrunde liegenden Ausgabestream schreibt". Wenn Sie aufhören zu schreiben und die Ausgabedatei vor dem Schließen lesen, sitzt der Schwanz Ihrer Daten noch im Kodierer, und die Datei sieht abgeschnitten aus. Deshalb schließt das Beispiel packed, bevor irgendetwas anderes die Datei berührt, und in Produktion würden Sie beide Streams in einen try-with-resources-Block legen. Holen Sie sich die Angewohnheit: Auf dem Kodierungs-Stream ist das Schließen Teil des Kodierens.

Das Treffen mit der alten Garde

Ererbte Codebasen sind voller Base64-APIs, die älter sind als java.util.Base64, und sie zu erkennen erspart Ihnen die "warum bricht das meine Ausgabe um"-Mysterien. Die vier, die Sie tatsächlich treffen werden:

API Wo Sie sie treffen werden Was zu tun ist
sun.misc.BASE64Encoder / BASE64Decoder Code vor Java 8 Migration zu java.util.Base64; in Java 9 entfernt
javax.xml.bind.DatatypeConverter Code aus der XML-Ära, alte Web Services In Java 11 entfernt (JEP 320); migrieren
org.apache.commons.codec.binary.Base64 Code, der auf Pre-8-JVMs laufen muss Für Pre-8-Unterstützung beibehalten; andernfalls ist die JDK-Klasse der Standard
com.google.common.io.BaseEncoding Guava-lastige und Big-Data-Stacks Funktioniert gut; die JDK-Klasse hat keine Abhängigkeiten

Das sun.misc-Paar ist dasjenige mit dem Drama. Es war eine interne, nicht unterstützte API (die Art, die auf dem JDK des Tages gut kompiliert und ohne Deprecation-Warnung verschwindet), und seine Ausgabe hatte eigene Angewohnheiten, wie das Umbrechen des kodierten Textes, woher eine überraschende Anzahl von "mein Base64 hat Zeilenumbrüche drin"-Bugs kommt. Als Java 9 im September 2017 erschien, entfernte die Modul-System-Aufräumaktion es, und der offizielle Migrationsleitfaden ist dabei ungeschminkt: "Besonders hervorzuheben: sun.misc.BASE64Encoder und sun.misc.BASE64Decoder wurden entfernt. Stattdessen die unterstützte java.util.Base64-Klasse verwenden, die in JDK 8 hinzugefügt wurde". Wenn Sie jdeps auf Code ausführen, der die alten Klassen noch referenziert, markiert das Tool die Abhängigkeit als "JDK removed internal API", was so nah an einem Verkehrskegel ist, wie es beim JDK wird. Der DatatypeConverter von JAXB hatte ein längeres, aber ähnliches Leben, in der Java-9-Ära mit den Java-EE-Modulen deprecated und in Java 11 durch JEP 320, "Die Java-EE- und CORBA-Module entfernen", gänzlich entfernt. Beide Migrationen sind mechanisch: Die alten printBase64Binary- und BASE64Encoder().encode-Aufrufe mappen eins-zu-eins auf getEncoder().encodeToString, abgesehen von den Umbruch-Unterschieden, und sobald der Code auf java.util.Base64 läuft, läuft er auf jedem JDK von 8 bis 26 ohne weiteres Nachdenken.

Sicherheit und Geschwindigkeit

Der Sicherheitsabschnitt ist kurz, denn der Job des Kodierers kann an Daten nicht scheitern, aber er ist nicht leer. Base64 ist keine Verschlüsselung, und der Standard sagt es mit so vielen Worten: Base-Kodierung "versteckt visuell sonst leicht erkennbare Informationen wie Passwörter, bietet aber keine rechnerische Vertraulichkeit", und derselbe Abschnitt bemerkt, dass dies "bekanntlich Sicherheitsvorfälle verursacht hat". Die praktischen Folgerungen für die Kodierer-Seite: Kodieren Sie kein Geheimnis, um es sicher zu machen (es ist jetzt weniger sicher, denn es passt in mehr Kanäle); wenn der Wert ein Geheimnis ist, verschlüsseln Sie zuerst und kodieren Sie den Chiffretext; und denken Sie an den Verformbarkeits-Zwilling, bei dem ein Empfänger eine gültige Schreibweise durch eine andere tauschen kann (anderes Padding, Müll in den Reserve-Bits), ohne die dekodierten Daten zu verändern. Ein deterministischer Kodierer hilft hier: java.util.Base64 produziert genau eine Ausgabe für genau eine Eingabe, also ist die Schreibweise stabil, wenn Ihr eigenes System einen Wert sowohl schreibt als auch liest, und es sind die externen Werte an der Vertrauensgrenze, die auf kanonische Form geprüft werden müssen.

Bei der Geschwindigkeit hat die Kodierer-Seite dieselbe Geschichte wie die Dekodierer-Seite: Auf einer modernen JVM ist die eingebaute Implementierung schnell genug, dass Base64 fast nie der Flaschenhals ist, und sie ist der Referenzpunkt des Benchmarks. Derselbe 2025er-gRPC-java-Benchmark, der im Schwester-Leitfaden erwähnt wurde (Issue 11857, JMH auf JDK 17 und 21), setzte den JDK-Kodierer auf etwa 2,5- bis 3,8-mal den Durchsatz von Guava, mit dem größten Abstand auf x86. Zwei praktische Notizen: Auf heißen Pfaden teilen Sie eine Kodierer-Instanz (die Fabrik gibt bereits dieselbe geteilte zurück) und bevorzugen Sie encode(byte[], byte[]) in ein vorgroßgezogenes Array, um die Allokation zu überspringen; für riesige Daten ist der Streaming-Abschnitt die Speicher-Geschichte, und die Kosten des Umbruchs sind Rauschen neben der Festplatte. Die einzige echte Performance-Abgabe bei Base64 ist die Größe selbst, und keine Implementierung, einschließlich dieser, kann sie verhandeln.

Die Fallen-Checkliste

Jede Falle an einem Ort gesammelt, alle Java-spezifisch:

  • Der fehlende Zeichensatz. text.getBytes() ohne expliziten Zeichensatz verwendet den Plattform-Standard: Zufällig richtig auf JDK 18+, falsch auf allem älteren, und prinzipiell überall falsch. Übergeben Sie StandardCharsets.UTF_8 und schreiben Sie den Zeichensatz in die Spezifikation.
  • Der gepaddete JWT. getUrlEncoder() padet standardmäßig, und Tokens wollen kein Padding. Der withoutPadding()-Aufruf ist Teil des Rezepts, kein optionales Extra; ein Token mit anhängenden = ist ein Token, das manche Validatoren ablehnen und manche zerzausen.
  • Die umgebrochene Ausgabe. Der MIME-Kodierer bricht bei 76 mit CRLF um und fügt keinen anhängenden Zeilenumbruch hinzu. Wenn der Verbraucher einen anhängenden Umbruch erwartet, fügen Sie ihn hinzu; wenn der Verbraucher gar keine Umbrüche erwartet, verwenden Sie nicht den MIME-Kodierer.
  • Die doppelte Kodierung. Einen Wert zu kodieren, der bereits Base64 ist, erzeugt einen vollkommen gültigen, vollkommen nutzlosen String. Die klassische Ursache: Ein Feld trifft vor-kodiert von einer API ein, und Ihr Code "hilft", indem er es noch einmal kodiert. Prüfen Sie, bevor Sie kodieren.
  • Pluszeichen in URLs. Standard-Base64-Ausgabe enthält +, das in einem Query-String ein Leerzeichen ist, bevor der Server es je sieht. Wenn ein Wert mit Standardalphabet in einer URL reisen muss, prozent-kodieren Sie ihn, oder erzeugen Sie ihn gleich im URL-sicheren Alphabet.
  • Die 33-Prozent-Rechnung. Ein Wert, der in die rohe Spalte passt, passt nicht in die kodierte. Dimensionieren Sie Speicher, Nachrichtenfelder und Header nach 4 * ceil(n / 3), und denken Sie daran, dass umgebrochene MIME-Ausgabe ein paar Prozent darüber hinaus ist.
  • Der nicht geschlossene Stream. Der verpackte Ausgabestream hält verbleibende Bytes bis zum Schließen. Die Datei vor dem Schließen zu lesen ergibt eine abgeschnittene Kodierung. Try-with-resources, jedes Mal.
  • Geheimnisse in vollem Licht. Base64 ist Paketband, kein Schloss. Kodierte Credentials in einer Konfigurationsdatei, einem Log oder einer Env-Var sind lesbare Credentials. Erst verschlüsseln, oder gar nicht.
  • Die Android-Mauer. Auf Android existiert java.util.Base64 erst ab API-Stufe 26; darunter ist die Framework-Klasse android.util.Base64 mit ihren eigenen Flag-Konstanten (NO_PADDING, URL_SAFE und NO_WRAP). Das eine oder das andere ohne Prüfung fest zu coden bricht genau auf den Geräten, die Sie nie getestet haben.
  • Die Zeilenlängen-Kuriosität. getMimeEncoder(77, ...) bricht still und leise bei 76 um, denn die Länge wird auf ein Vielfaches von vier abgerundet, und eine Bitte um 3 oder weniger deaktiviert den Umbruch ganz. Wenn Ihr Format eine ungerade Zeilenlänge verlangt, ist der MIME-Regler nicht das Werkzeug.

Von sun.misc zur Standardbibliothek

Die Java-Geschichte ist eine kurze mit klarem Vorher und Nachher. Vor 2014 bekam man, wenn man Base64 im JDK brauchte, das interne Paar sun.misc.BASE64Encoder und sun.misc.BASE64Decoder, von Tag eins an nicht unterstützt, mit ihren eigenen 76-Zeichen-Umbruch-Angewohnheiten, oder man griff in XML-Code nach javax.xml.bind.DatatypeConverter, oder man fügte Apache Commons Codec oder Guava dem Build hinzu, und so endeten viele Unternehmens-Codebasen mit drei Base64-Implementierungen und ohne Ahnung, welche war welche. Am 18. März 2014 brachte Java 8 java.util.Base64: eine Klasse, drei Alphabete, die RFC-4648- und RFC-2045-Regeln ordentlich implementiert, das Factory-Muster, die Padding- und Umbruch-Regler, und Stream-Adapter in beide Richtungen. Es war das Base64, das die Sprache von Anfang an hätte haben sollen, und die Javadoc sagt seitdem Since: 1.8.

Die Aufräumaktion kam in zwei Wellen. Java 9 (21. September 2017) entfernte das sun.misc-Paar als Teil der Modul-System-Aufräumaktion, mit dem Migrationsleitfaden, der jeden Entwickler auf die JDK-8-Klasse verweist, und Java 11 entfernte das JAXB-Modul samt dessen DatatypeConverter (JEP 320). Java 18 (22. März 2022) landete JEP 400, "UTF-8 als Standard", der Base64 überhaupt nicht berührte, aber den Fehlermodus der faulen getBytes()-Aufrufe, die ihn speisen, veränderte: Der Standard-Zeichensatz der Plattform wurde auf jedem OS UTF-8, also hörten alte Mojibake-Muster einfach auf, sich auf neuen JVMs zu reproduzieren. Seit 1.8 hat sich die öffentliche API um keine einzige Methode geändert. Bewegt hat sich der Motor darunter: Bug-Fixes und Performance-Arbeit, und deshalb finden Community-Benchmarks immer wieder, dass die Standardbibliotheks-Version die Legacy-Bibliotheken, die sie ersetzte, ausfliegt. Heute, auf jedem JDK von 8 bis 26, ist die Antwort auf "Wie base64-ich das in Java" ein Import und ein Factory-Aufruf, und es ist das seit über einem Jahrzehnt.

Ein paar Nerd-Freuden

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

  • Die Javadoc sagt Since: 1.8, und das ist seit zwölf Jahren wahr. Nicht eine Methode hinzugefügt, keine entfernt, kein Verhalten geändert: Eine der am längsten eingefrorenen API-Oberflächen der Sprache, und Sie verwenden sie ohne nachzudenken.
  • encodeToString baut ihren Ergebnis-String mit dem ISO-8859-1-Zeichensatz, gemäß der Javadoc. In der Praxis ist es ein vollkommen unnötiges Detail, denn Base64-Ausgabe ist reines ASCII und sieht in Latin-1, UTF-8 und dem größten Teil des übrigen Zeichensatz-Zoos gleich aus, aber die Javadoc sagt es trotzdem, was der JDK halt der JDK ist.
  • Der MIME-Kodierer fügt nach der letzten unvollständigen Zeile keinen Zeilentrenner hinzu. Andere Tools, einschließlich einiger sehr berühmter E-Mail-Bibliotheken, enden umgebrochene Ausgabe mit einem anhängenden CRLF. Wenn Ihr Diff gegen eine Referenz-Implementierung genau zwei Zeichen am Ende ist, haben Sie diese Kuriosität gefunden.
  • Bitten Sie getMimeEncoder um 77-Zeichen-Zeilen, und er gibt Ihnen 76: Die Zeilenlänge wird still und leise auf das nächstniedrigere Vielfache von vier abgerundet, denn ein Umbruch, der eine Vier-Zeichen-Gruppe teilt, würde Müll produzieren. Die API verweigert es, eine kaputte Zeile zu bauen, statt Ihre Erlaubnis zu fragen.
  • Base64.getEncoder() == Base64.getEncoder() ist wahr. Die Factory-Methoden geben bei jedem Aufruf dieselbe geteilte Instanz zurück, also ist die "holen Sie sich eine neue"-API ein Kostüm für einen Singleton, und das Versprechen der Thread-Sicherheit ist nur eine Beschreibung dessen, was die JVM ohnehin schon tut.
  • Auf Android stellt die Zwilling-API android.util.Base64 dieselben Entscheidungen als Flags dar: NO_PADDING, URL_SAFE, NO_WRAP. Zwei APIs, eine Entscheidungstabelle, was eine stille Bestätigung dafür ist, wie ausgereift das Base64-Design inzwischen ist.
  • Abschnitt 5 von RFC 4648 ist der Ort, an dem der Name "base64url" geboren wird: Die Spezifikation sagt, die URL-sichere Kodierung "kann als base64url bezeichnet werden", und warnt, sie "sollte nicht als dasselbe wie die base64-Kodierung betrachtet werden". Ihr Ursprung ist mit einer Fußnote auf einen 2001er-Post in einer P2P-hackers-Mailingliste verwiesen, also hat der Name in jeder URL, die Sie kleben, eine Mailingliste-Herkunft.
  • Kodieren Sie das Wort base64, und Sie bekommen YmFzZTY0, kein Padding, denn sechs ist ein Vielfaches von drei. Ein Format, das sich selbst beschreibt, ist das technische Äquivalent eines Spiegels, der in Morse spricht, und dies ist die eigene Spiegelung des Spiegels.
  • Führen Sie jdeps -jdkinternals auf Code vor Java 8 aus und sehen Sie zu, wie es sun.misc.BASE64Encoder als "JDK removed internal API" markiert. Das Beispiel des Tools im offiziellen Migrationsleitfaden ist eine Base64-Klasse, was der JDK ist, der auf Ihre Imports zeigt und sagt "wir haben darüber gesprochen".
  • Der 1,37-Faktor. Jeder umgebrochene MIME-Payload kostet etwa 1,37-mal seine Originalgröße (4/3 für das Alphabet, 78/76 für den CRLF-Rhythmus), ein so stabiler Bruchteil, dass die alte E-Mail-Rechnung ihn noch immer zitiert: Die Maut, die die Mail-Infrastruktur der 1990er auf jeden Anhang erhob, ist exakt die Rechnung, die getMimeEncoder() heute erhebt.

In die andere Richtung

Das war die Kodierer-Seite der Geschichte, und sie ist die ruhigere der beiden: Der Job scheitert nie an den Daten, die Fallen drehen sich um Ihre Entscheidungen (Zeichensatz, Padding, Umbruch, Dialekt) und nicht um die Überraschungen anderer Leute, und die gesamte API passt in einen Import. Die andere Richtung ist der Ort, an dem Base64 aufhört bequem zu sein und anfängt adversarial zu werden, denn beim Dekodieren treffen Sie auf die Padding-Wahlen anderer Leute, ihre Zeilenumbrüche, ihre Zeichensätze und ihre Rüstung, mit einer IllegalArgumentException, die zwischen Ihnen und der Wahrheit steht. Base64-Dekodierung in Java, von dieser Seite verlinkt, behandelt den Dekodierer mit derselben Tiefe: Die drei Dekodierer-Persönlichkeiten, die exakten Fehlermeldungen, die Padding-Regeln, base64url und JWTs, MIME und PEM, und die Java-spezifischen Fallen, an einem Ort gesammelt. Lesen Sie die beiden als Paar, und das ganze Thema gehört Ihnen.

Zuletzt aktualisiert: 2026-09-08

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