Decodifica Base64 in Java: una guida completa
Arriva in un ticket di supporto, in una risposta API, in un segreto di Kubernetes, o sepolto in mezzo a un URL: un lungo filo di lettere e cifre con qua e là un +, /, - o _, e forse uno o due segni = appesi alla fine. Qualcuno dice che è Base64 e che contiene qualcosa che ti serve: una password, un payload JSON, un certificato, una foto. Questa guida è la ricetta Java per riaverlo. Brevissimo orientamento, perché la home page spiega il formato nel dettaglio: Base64 riscrive ogni tre byte di dati come quattro caratteri tratti da un alfabeto di 64 lettere, e appiccica uno o due pad = alla fine quando l'ultimo pezzo è corto. Decodificare è la direzione in cui questo scambio si rimpicciolisce: entrano quattro caratteri, escono tre byte, quindi il risultato richiede sempre circa un quarto in meno di spazio rispetto all'input.
Ecco la notizia principale, ed è una buona notizia. Dal 18 marzo 2014 ogni JDK include nella libreria standard una cassetta degli attrezzi Base64 completa: java.util.Base64. Nessun download, nessuna coordinata Maven, nessuna libreria nativa. Un import, sette metodi factory, tre alfabeti, e lo stesso comportamento dal Java 8 di allora al Java 26 di oggi. Tutto in questo articolo si regge su quella singola classe.
Un confine onesto prima di iniziare: questa è la parte decoder della storia. Imparerai a scegliere il decoder giusto per l'alfabeto che incontri, a leggere i messaggi d'errore del JDK come un medico legge una radiografia, a trasformare byte in testo senza mojibake, a togliere l'armatura PEM, a gestire in streaming payload da giga byte, e a riconoscere le trappole di sicurezza che il formato lascia di nascosto sulla strada. L'altra direzione, impacchettare byte in una stringa, ha la sua guida, ed è collegata in fondo a questa.
Cosa hai già
Installare Base64 in Java è la risposta in una riga che dai alla lavagna: "È nel JDK." La classe java.util.Base64 fa parte del modulo java.base dal 1.8, e il suo javadoc dice ancora Since: 1.8 nel 2026. L'unica cosa che installi è un JDK, qualsiasi Java 8 o più recente di qualsiasi produttore (Oracle, Eclipse Temurin, Amazon Corretto, Zulu) va bene, e su una macchina basata su Debian è un solo comando:
sudo apt install openjdk-17-jdk-headless
L'API è una factory: non costruisci mai un decoder; lo chiedi alla classe. I sette metodi factory distribuiscono tre personalità in ciascuna direzione, e il lato decoder è questo:
| Metodo factory | Alfabeto | Umore | Quando usarlo |
|---|---|---|---|
getDecoder() |
A-Z a-z 0-9 + / |
Rigoroso: rifiuta qualsiasi carattere fuori alfabeto | Dati che produci o controlli |
getUrlDecoder() |
A-Z a-z 0-9 - _ |
Rigoroso, alfabeto URL-safe | JWT, token, ID, qualsiasi cosa nata in un URL |
getMimeDecoder() |
A-Z a-z 0-9 + / |
Tollerante: salta ogni carattere non dell'alfabeto | Email, input davvero avvolti, corpi PEM |
getEncoder(), getUrlEncoder(), getMimeEncoder() |
come sopra | Codifica, territorio della guida sorella | Ogni volta che produci Base64 invece di leggerlo |
Tre proprietà delle istanze restituite valgono la memoria. Prima, sono thread-safe: il javadoc dice che le istanze sono "sicure per l'uso da parte di più thread concorrenti", e il codice sorgente mostra i metodi factory che restituiscono la stessa istanza condivisa a ogni chiamata, quindi Base64.getDecoder() == Base64.getDecoder() è vero. Costruisci un decoder in un campo statico e condividilo in tutto il servizio; non stai nemmeno copiando nulla. Secondo, sono senza stato tra una chiamata e l'altra, quindi non c'è nulla da resettare e nulla da sincronizzare. Terzo, passare null dove ci si aspetta un array di byte o una stringa non è un'operazione nulla innocua: è un NullPointerException, esattamente come promette il javadoc della classe.
Continuerai a incontrare librerie più vecchie nei codebase, quindi una mappa rapida del panorama. Apache Commons Codec (attualmente 1.22.1) porta la sua org.apache.commons.codec.binary.Base64 dal 1.0, con un'API Builder che espone come perille la politica rigoroso-o-tollerante, la lunghezza delle righe e il separatore; è lo strumento giusto solo se devi supportare JVM pre-Java-8 o vuoi i suoi helper di controllo della forma. Guava offre com.google.common.io.BaseEncoding, un veterano dalle capacità simili, ancora popolare negli stack big data. Per tutto ciò che gira su una JVM moderna, java.util.Base64 è la scelta di default: zero dipendenze, e i benchmark della comunità continuano a trovarlo il più veloce del gruppo (ne parliamo nella sezione sulle prestazioni).
Decodificare la tua prima stringa
Novanta percento della vita di chi decodifica sta in un pugno di righe. Ecco l'intera cerimonia, usando l'esempio più piccolo che lo stesso RFC usa per spiegare l'alfabeto:
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
}
}
Quattro frasi su quello che è appena successo. Prima, il punto d'ingresso è un'istanza, non la classe: decode() vive sull'oggetto Base64.Decoder che hai preso dalla factory. Secondo, ed è la decisione di design più importante di tutta l'API, il risultato è un array di byte, mai una String. Il payload può essere una frase, un JPEG o un hash, e nessuno dei tre va trattato allo stesso modo prima di sapere cosa hai, quindi il JDK si ferma ai byte, apposta. Terzo, il salto da byte a testo è un passo separato, deliberato, con un charset esplicito, ed è proprio lì che "café" diventa mojibake se fai finta di niente; la sezione sui charset qui sotto è dedicata a questo. Quarto, la stringa vuota è un valore di prima classe: Base64.getDecoder().decode("") ti restituisce un array di lunghezza zero, nessuna eccezione, nessun dramma.
Per dati di prova a memoria, ricorda che TWFu è il test di fumo dello standard stesso: se il tuo codice di decodifica lo trasforma in Man, la macchina è onesta. Il viaggio nell'altra direzione richiede due righe della stessa API e viene trattato nel dettaglio nella guida sulla codifica collegata in fondo.
La linea dei decoder
Java non ti dà un decoder; te ne dà tre, e la differenza tra loro è una decisione di politica: quale alfabeto accettare e quanto disordine tollerare. I tre sono istanze della stessa classe annidata Base64.Decoder. Il javadoc della classe esplicita la divisione con una frase per umore. Per i decoder base e URL-safe: il decoder "rifiuta dati che contengono caratteri fuori dall'alfabeto base64". Per il decoder MIME: "tutti i separatori di riga e altri caratteri non presenti nella tabella dell'alfabeto base64 sono ignorati nell'operazione di decodifica". Quella seconda frase è l'intera storia MIME in una riga, e ha i denti, perché "ignorati" significa tutto ciò che non è un carattere dell'alfabeto, non solo gli a capo.
La regola di scelta è corta. Parti da getDecoder(). Se il valore viene da un URL, da un token o da un'API che prometteva "URL-safe", passa a getUrlDecoder(). Solo se ti aspetti davvero input di forma MIME (a capo ogni 76 caratteri, direttamente da un sistema di posta) raggiungi getMimeDecoder(). In caso di dubbio, scegli il rigoroso: il lavoro di un decoder rigoroso è far fallire le sorprese, ed è esattamente ciò che vuoi a un confine di fiducia. Un decoder tollerante, d'altra parte, è una lente d'ingrandimento per la corruzione: una stringa con caratteri vaganti dentro verrà decodificata in qualcosa di plausibile e sbagliato, senza alcun errore.
Leggere i reclami del decoder
I decoder rigorosi falliscono forte, e falliscono con precisione. Ogni input sbagliato lancia un IllegalArgumentException il cui messaggio ti dice esattamente cosa è andato storto, quindi la prima volta che una stringa di produzione esplode, questa tabella è quello che leggi. I messaggi sotto sono il testo esatto del JDK attuale:
| Input (a getDecoder salvo nota) | Cosa non va | Messaggio esatto |
|---|---|---|
"SGVs bG8s" |
uno spazio si è infilato | Illegal base64 character 20 |
"SGVs\nbG8s" |
un a capo si è infilato | Illegal base64 character a |
"SGVs$bG8s" |
il segno di dollaro non è nell'alfabeto | Illegal base64 character 24 |
"SGVsbG8-" |
un trattino URL-safe nel decoder standard | Illegal base64 character 2d |
"ab+c" a getUrlDecoder() |
un segno più nel decoder URL-safe | Illegal base64 character 2b |
"S" |
un simbolo non può formare un byte | Input byte[] should at least have 2 bytes for base64 bytes |
"SG=VsbG8s" |
padding in mezzo ai dati | Input byte array has wrong 4-byte ending unit |
"Zm8==" |
due pad dove ne spetta uno | Input byte array has incorrect ending byte at 4 |
"Z=" |
un carattere seguito da un pad | Last unit does not have enough valid bits |
"SGVsbG8sIHdvcmxkIQ==xx" |
spazzatura dopo i pad | Input byte array has incorrect ending byte at 20 |
Quel numero esadecimale nel messaggio è il valore del byte del carattere colpevole, stampato con Integer.toString(byte, 16): 20 è uno spazio, a è un line feed, d è un carriage return, 24 è il segno di dollaro, 2d è il trattino URL-safe, 2b è il più, 2f è la barra, 5f è il trattino basso. Due stranezze da tenere in tasca. Prima, il messaggio può andare negativo: dai in pasto al decoder una stringa che contiene é e si lamerà Illegal base64 character -17, perché il carattere viene prima mappato sul byte Latin-1 0xE9, che come byte Java con segno vale meno 23, e meno 23 in esadecimale è meno 17. Il tuo log degli errori, per un momento, sta facendo aritmetica con segno. Secondo, la posizione: nella famiglia incorrect ending byte at N, N è l'indice a partire da zero del primo byte che il decoder non è riuscito a interpretare, ed è un regalo quando stai bisezionando un payload corrotto.
Un cambio di costume da conoscere: quando la decodifica avviene attraverso lo stream avvolto (la variante wrap(InputStream), trattata più sotto), gli stessi problemi affiorano come IOException con il prefisso 0x al posto del solito: Illegal base64 character 0x20 (JDK attuali; lo stream decoder del JDK 8 stampa il valore recuperato, -1, al posto del byte). Stesso problema, eccezione diversa, ortografia leggermente diversa. E il decoder MIME tollerante, naturalmente, non si lamenta di nessuna di queste cose: salta soltanto. Questo è il prezzo dell'umore tollerante.
Le regole del padding
Ogni stringa Base64 nel mondo fa una promessa silenziosa sul padding, e la promessa di Java è insolitamente amichevole. Il javadoc del decoder lo dice alla lettera: il carattere di padding = "viene accettato e interpretato come fine dei dati byte codificati, ma non è obbligatorio". Un'ultima unità di due o tre caratteri viene decodificata come se fosse stata provvista di padding, e quando i pad sono presenti devono esserlo nella quantità esattamente giusta. Il comportamento del JDK attuale sui classici esempi:
| Input | Risultato |
|---|---|
"" |
array di byte vuoto, nessun errore |
"Zm8" |
"fo", padding semplicemente assente |
"Zm8=" |
"fo", la notazione canonica |
"Zm8==" |
IllegalArgumentException: incorrect ending byte at 4 |
"Zm9v=" |
IllegalArgumentException: wrong 4-byte ending unit |
"Zg==" |
"f", un byte |
"Z=" |
IllegalArgumentException: last unit does not have enough valid bits |
"AA==" |
esattamente un byte, il byte NUL 0x00 |
"AAAA" |
tre byte NUL |
Leggi quella tabella due volte. La stringa vuota si decodifica nel nulla, mentre AA== si decodifica in un singolo byte NUL: nel Base64, "niente" e "uno zero" sono creature diverse, e sono entrambe input perfettamente validi. E il padding, quando presente, deve essere esatto: Zm8= è giusto, Zm8== è sbagliato, Zm9v= è sbagliato, e un pad in mezzo alla stringa è sbagliato. Conseguenza pratica per i tuoi protocolli: scegli una notazione (con pad o senza) e applicala su entrambi i lati, perché un valore che può arrivare in due notazioni è un valore che può far cadere un controllo di uguaglianza ingenuo più avanti sulla strada.
base64url: l'alfabeto costruito per gli URL
Il Base64 standard chiude il suo alfabeto con + e /, e sono esattamente i due caratteri che non si comportano bene negli URL: un + in una query string è già uno spazio prima che Java lo veda, una / è un separatore di percorso, e un = appeso vuole la codifica percent e diventa un mostro di tre caratteri. La sezione 5 del RFC 4648 disegna la correzione: l'alfabeto sicuro per URL e nomi di file, dove + diventa -, / diventa _, e il padding di coda = viene di solito buttato via quando la lunghezza è nota implicitamente. Il RFC è inflessibile sul nome: questa codifica "non dovrebbe essere considerata la stessa della codifica base64". Lo incontrerai come base64url, ed è là che vivono i JSON Web Token, i parametri state di OAuth, gli ID di sessione API e gli ID video di undici caratteri.
Il payload base64url più famoso sul web è il JWT, e dare un'occhiata dentro ne costa tre righe. Le parti di un token sono per convenzione senza padding, e il decoder URL se ne sta benissimo, perché il padding è accettato ma non richiesto, ricordalo:
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}
}
}
Qui vivono due avvertimenti onesti. Prima, decodificare un JWT è dare un'occhiata, non fidarsi: la terza parte è una firma, e le due parti che hai appena letto non sono segrete e non sono autenticate. Fidarsi di un payload prima di verificarne la firma è il classico bug dei JWT, e la correzione è affidare la verifica a una libreria JOSE come JJWT (0.13.0) o nimbus-jose-jwt (10.9.1) invece di costruire la tua criptografia. Secondo, gli errori identificano la direzione: dai una stringa a alfabeto standard a getUrlDecoder() e ottieni Illegal base64 character 2b o 2f, e il contrario ti procura 2d o 5f. L'alfabeto sbagliato è il singolo fallimento di decodifica Base64 più comune nel mondo, e il messaggio d'errore lo indica in un battito di ciglia. Se un token in una query string doveva essere Base64 standard, i suoi + e / sono stati probabilmente rovinati dal trasporto prima ancora di arrivare a te, e l'errore di decodifica ti sta parlando di un bug a monte, non del tuo decoder.
Dai byte alle parole
Ogni chiamata di decodifica in questo articolo si ferma ai byte apposta, perché il Base64 è un formato di byte, punto. La domanda "che testo era?" tocca a te rispondere, e la risposta di default moderna è UTF-8. C'è però un dettaglio di charset sul lato decodifica dell'API che sorprende la gente, eccolo. L'overload decode(String) non interpreta la tua stringa come UTF-8. Il javadoc lo dice alla lettera: una chiamata "ha esattamente lo stesso effetto di chiamare decode(src.getBytes(StandardCharsets.ISO_8859_1))". Non è un bug - è un trucco: l'alfabeto Base64 è ASCII puro, quindi far passare la stringa dal Latin-1 passa al decoder esattamente gli stessi byte a costo di conversione zero, e qualsiasi carattere non ASCII nell'input diventa semplicemente un simbolo non valido che il decoder rigoroso rifiuta (da lì vengono i numeri esadecimali negativi nei messaggi d'errore).
Il charset del payload è una decisione completamente separata, quella del passo new String(bytes, charset). Ecco il caso classico: "café" in UTF-8 sono i cinque byte 63 61 66 C3 A9, che codificano in Y2Fmw6k=:
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é, l'accento sopravvive
System.out.println(new String(packed, StandardCharsets.ISO_8859_1));
// caf seguito da mojibake, i byte UTF-8 letti male come Latin-1
}
}
Quella seconda riga è il modo di fallimento da riconoscere all'istante: un payload UTF-8 letto dal Latin-1, che produce una stringa lunga esattamente un carattere in più e fuori di un byte. La cura è sempre accordarsi su un charset con chi produce e passarlo esplicitamente. E passarlo esplicitamente nel codice, non solo in testa: il costruttore senza argomenti new String(bytes) usa il charset di default della piattaforma, che su un server Windows può essere Cp1252 e su un Linux vecchio può essere quello che la macchina vuole. Dal JDK 18 (JEP 400, "UTF-8 di default") il default è UTF-8 su ogni piattaforma, quindi su una JVM moderna la forma senza argomenti casca nel giusto, ma il tuo codice dovrebbe dirlo comunque, perché chi lo leggerà dopo non dovrebbe dover sapere qual è il default. E quando il payload non è testo per niente, lo stesso codice ha solo un finale diverso: byte dentro, byte fuori, fino all'ultimo passo.
Quando il payload è un file
Il lavoro sui file più comune è l'inverso di qualche routine di esportazione: arriva un file di testo .b64, e ti serve di nuovo il file originale. Con la decodifica rigorosa, è già in forma da produzione:
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);
}
}
Nulla in questo percorso si cura se il payload è un file di testo, un archivio ZIP o un video: byte[] sono solo byte. Il calcolo delle dimensioni gioca a tuo favore, anche: l'output decodificato è tre quarti della lunghezza dell'input codificato, quindi decodificare non peggiora mai la memoria, e un file codificato di centinaia di megabyte è il più piccolo dei due. Un'abitudine buona è lasciare che i byte si annuncino da soli prima di fidarti di qualsiasi etichetta. I primi otto byte di un PNG sono sempre il numero magico 89 50 4E 47 0D 0A 1A 0A, il che significa che ogni PNG codificato in Base64 che incontrerai comincia con lo stesso prefisso, iVBORw0K: se un payload "dichiara" di essere un'immagine e non comincia così, qualcosa è già andato storto.
Se possiedi già il buffer di destinazione, l'overload a due array scrive direttamente dentro e restituisce esattamente quanti byte sono atterrati, senza allocazione intermedia:
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!
}
}
Un angolo spigoloso su quell'overload, documentato nel javadoc: se la destinazione è troppo piccola, nessun byte viene scritto e ottieni IllegalArgumentException: Output byte array is too small for decoding all input bytes. Dimensiona il buffer con la matematica semplice, circa 3 * n / 4 meno il padding, e l'eccezione non mostra mai il muso. C'è anche un overload ByteBuffer che restituisce un buffer fresco con il limite impostato alla lunghezza decodificata, comodo quando il tuo pipeline vive in NIO.
Sulla rete: header, JSON e data URI
Il Base64 incontra il Java più spesso sul bordo di rete. Tre forme meritano ognuna un esempio svolto.
Forma uno: l'header di autenticazione Basic HTTP. L'header di autenticazione più vecchio del web viaggia ancora su Base64. Secondo il RFC 7617, una richiesta Basic invia Authorization: Basic seguito dalla codifica Base64 di username:password, e il RFC è esplicito che questa è codifica, non protezione: chiunque abbia una cattura di pacchetti può leggere entrambe le metà in un colpo. L'esempio dello stesso RFC, QWxhZGRpbjpvcGVuIHNlc2FtZQ==, si decodifica in Aladdin:open sesame. Analizzare l'header lato server è un lavoro di poche righe:
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)};
}
}
Due dettagli tengono al sicuro questa cosa. La divisione al primo due punti conta, perché una password può legalmente contenere due punti propri. E il confronto della password decodificata con il valore che hai in archivio dovrebbe essere a tempo costante: calcola l'hash di entrambi i valori con SHA-256 e confronta i digest con MessageDigest.isEqual, mai un equals semplice che un attaccante può cronometrare fino a ottenere una lista di utenti. Servi questo solo su HTTPS; su una connessione in chiaro lo strato Base64 è solo ornamento.
Forma due: binario dentro il JSON. Una grossa quota delle API moderne incorpora il binario come testo Base64 dentro il JSON: endpoint di upload file, API di contenuto, archivi di segreti e webhook lo fanno tutti, perché i byte grezzi altrimenti romperebbero le regole di escape della stringa JSON. Lo schema è sempre lo stesso: il campo arriva come stringa normale, e lo decodifichi al confine, non dentro i tuoi oggetti di dominio:
import java.util.Base64;
public class ApiField {
public static void main(String[] args) {
// Il JSON analizzato conteneva: "content" : "iVBORw0KGgoAAA..."
String field = "iVBORw0KGgo=";
byte[] image = Base64.getUrlDecoder().decode(field);
// Alcune API parlano Base64 standard invece. Leggi la specifica,
// poi scegli getDecoder() o getUrlDecoder() di conseguenza.
System.out.println(image.length); // 8
}
}
L'insidia qui non è decodificare; è leggere la specifica. Alcune API vogliono Base64 standard con padding, altre vogliono base64url senza, e poche sono tolleranti con entrambe. Quando la specifica tace, la correzione più economica è guardare un valore di esempio dall'altra parte: un - o un _ in qualunque punto del valore chiude la questione dell'alfabeto, e un = di coda chiude quella del padding.
Forma tre: la data URI. Qualcuno incolla un'immagine in un modulo e il front end ti passa l'intera data URI: data:image/png;base64,iVBORw0KGgo.... Il RFC 2397 definisce la forma: data:, un media type facoltativo, un flag ;base64 facoltativo, una virgola, e poi i dati. Quando il flag è presente il payload è Base64; quando è assente il payload è testo normale con codifica percent, più raro ma legale. Se il media type è omesso, il default è text/plain;charset=US-ASCII. Spezzarne una è semplice:
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
}
}
Due insidie vivono in questo formato. La prima è il flag ;base64 mancante: una data URI legale senza il flag porta un payload con codifica percent, e passarlo per Base64.getDecoder() fa lanciare un'eccezione. La seconda è il media type dichiarato: è un indizio dal mittente, non un fatto, quindi controlla i byte magici di ciò che hai decodificato prima di archiviare sotto "png". E ricorda il consiglio dello stesso RFC che le data URI sono per valori corti; un'immagine da diversi megabyte dentro un URL è un cattivo odore, non uno schema.
Email, MIME e armatura PEM
Il Base64 è nato per la posta, e il Base64 di forma postale arriva ancora nei programmi Java continuamente. Lo standard MIME (RFC 2045) ha fatto del Base64 una delle codifiche di trasferimento binario e ha aggiunto due regole di casa: le righe codificate non devono superare i 76 caratteri, e i decoder devono ignorare ogni carattere fuori dall'alfabeto, i cambi di riga inclusi. I decoder rigorosi rifiutano il primissimo cambio di riga; getMimeDecoder() è stato costruito per esattamente questo input:
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
}
}
Funziona, e dovresti comunque conoscere la trappola, perché la trappola ha i denti. Il decoder tollerante non "ignora gli a capo"; ignora tutto ciò che non è nel suo alfabeto. Se una stringa Base64 standard viene corrotta da caratteri vaganti, la spazzatura sparisce e il resto si decodifica in qualcosa di plausibile, quindi raggiungi getMimeDecoder() solo quando ti aspetti davvero input di forma MIME.
Il fratello di MIME nel mondo è l'armatura PEM, la faccenda -----BEGIN CERTIFICATE----- che avvolge certificati e chiavi. Ecco la trappola: le righe dell'armatura sono piene di caratteri comuni dell'alfabeto. Le lettere di "BEGIN CERTIFICATE" sono solo lettere Base64, quindi dare in pasto un intero blocco PEM, armatura inclusa, decodifica l'armatura come se fosse dato. Rimuovi l'armatura tu, poi passa il corpo nudo a un decoder:
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); // il corpo DER, senza armatura
}
}
Il PEM per convenzione avvolge a 64 caratteri per riga (il MIME a 76), e una volta che gli spazi bianchi sono via, il decoder rigoroso e il decoder MIME concordano sul risultato. Usa il rigoroso: una sorpresa ha almeno la decenza di lanciare un'eccezione. Per il caso sporco ma standard, la ricetta classica è rimuovere gli spazi bianchi noti e decodificare con l'istanza rigorosa, e lasciare che qualsiasi spazzatura residua si guadagni un IllegalArgumentException invece di un certificato corrotto.
Configurazione, ambiente e valori di database
Il Base64 è un contenitore di testo, ed è per questo che si presenta in posti dove non te lo aspetti. Nei database, un blob binario (un file, un'icona, una struttura serializzata) può vivere in una colonna TEXT come Base64, sopravvivendo a ogni strumento che dà per scontato il testo; aspettati che il valore immagazzinato sia circa un terzo più grande dell'originale e dimensiona la colonna di conseguenza. Nei file di configurazione e nelle variabili d'ambiente, il Base64 è il trucco per contrabbandare valori che altrimenti romperebbero il formato: un DSN con punti e virgola, una password con virgolette, un certificato su più righe. Decodificare all'avvio è l'intero lavoro:
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 potrebbe essere: pg:host=db;password=qu"ote
}
}
La stessa cautela vale qui due volte. Prima, questa è sicurezza di formato, non segretezza: nel momento in cui uno sviluppatore legge il file di configurazione, può decodificare il valore in una chiamata, quindi non immagazzinare mai un segreto come Base64 e chiamarlo cifrato; la sezione sulla sicurezza qui sotto entra nel dettaglio. Secondo, convalida all'avvio: un valore di ambiente corrotto o incollato a metà è un IllegalArgumentException della chiamata rigorosa, e un controllo di due righe trasforma un errore di runtime criptico in un messaggio di avvio operativo. Una nota specifica per Java per la gente del database: tieni il binario decodificato come byte[] (un parametro byte[] nel tuo codice JDBC), e non fare mai viaggiare il binario in un String, perché i costruttori di stringa sono il posto dove i payload binari vanno a morire.
Lo streaming delle cose grandi
Per payload grandi ma che stanno ancora in un buffer che gestisci tu, le API ad array vanno bene. Per payload che non dovrebbero stare in memoria nemmeno lontanamente, l'adattatore a stream è la mossa: wrap(InputStream) restituisce uno stream di input che decodifica mentre leggi, quindi un file codificato di giga byte non deve mai stare in un array di byte:
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();
}
}
Due dettagli da sapere. I metodi di lettura dello stream avvolto lanciano IOException quando incontrano byte che non possono essere decodificati, quindi un file corrotto fallisce con un'eccezione di stream invece di un IllegalArgumentException. E chiudere lo stream avvolto chiude lo stream sottostante, quindi l'esempio chiude packed per ultimo, dopo il ciclo di copia, e in produzione metteresti entrambi in un blocco try-with-resources. (Il buffer 8192 è solo un generoso buffer di lettura; lo stream avvolto decodifica internamente, quindi la dimensione con cui leggi è una scelta di prestazioni, non un requisito di protocollo.)
Ora un avvertimento legacy, perché questo è l'unico vero bug in tutta la storia e ha un numero di bug. Su ogni JDK prima del 16 (il bug report lo riproduce su 8, 10 e 11), leggere un decoder avvolto con certe dimensioni di buffer aggiunge due byte zero vaganti alla fine dei dati decodificati: JDK 8222187, la cui riproduzione classica abbina un buffer di lettura di sette byte a un semplice input di otto byte, ed è stato corretto nel JDK 16. Se devi fare streaming su un legacy JDK 8, ricontrolla la lunghezza decodificata dopo la copia, perché il bug scatta per combinazioni particolari di input e buffer e anche un buffer di 4096 byte è stato segnalato in giro, o meglio ancora, aggiorna il JDK, che in fondo correggerebbe altre cento cose circa.
Nastro imballo, non una serratura
Ecco la sezione che separa i cauti dai bruciati. Il Base64 non è cifratura, e lo standard stesso lo dice due volte. Sezione 12 del RFC 4648: la codifica Base "nasconde visivamente informazioni altrimenti facilmente riconoscibili, come le password, ma non fornisce alcuna riservatezza computazionale", e prosegue notando che, quando qualcuno incolla uno scambio di protocollo in un ticket e rivela per sbaglio la password, questo meccanismo "è noto che ha causato incidenti di sicurezza". Il consiglio del RFC per chi implementa merita una cornice anche: "un decoder non deve rompersi su input non valido, inclusi ad esempio caratteri NUL incorporati".
La trappola più sottile è la malleabilità. Ricorda che ogni simbolo porta sei bit, e che un'ultima unità corta lascia bit di scarto che in una codifica ben formata devono essere zero. Un encoder disattento o ostile può mettere spazzatura in quei bit di scarto, e il risultato sembra ancora completamente valido: MQ== e MT== si decodificano entrambi nel singolo byte della cifra 1. Il Java prende il lato indulgente di questa storia: Base64.getDecoder().decode("MT==") non verifica i bit non significativi e ti passa allegramente lo stesso byte. Perché importarcelo? Perché due stringhe diverse che si decodificano negli stessi dati rompono l'assunto della "notazione unica" su cui contano di nascosto i controlli hash, la deduplicazione e i confronti di firma, e un attaccante che può alterare un valore codificato durante il transito può scambiare una notazione con l'altra. Il paper del 2022 "La malleabilità del Base64 in pratica" di Chatzigiannis e Chalkias (ACM ASIA CCS 2022) passa in rassegna esattamente queste inconsistenze tra implementazioni reali. Le parole dello stesso RFC sui bit di scarto: possono "essere oggetto di abuso per far perdere informazioni, o essere usati per aggirare i confronti di uguaglianza tra stringhe o per far scattare problemi di implementazione". La regola pratica non è "non decodificare mai"; è "conoscere il tuo confine": per dati tra i tuoi stessi sistemi la generosità del JDK va bene, ma per dati che attraversano un confine di fiducia, imponi la forma canonica (lunghezza corretta, bit di scarto a zero, una sola notazione del padding) prima di fidarti di ciò che hai decodificato.
Note sulle prestazioni
Ecco la buona notizia in una frase: su una JVM moderna, il decoder integrato è abbastanza veloce che il Base64 è quasi mai il tuo collo di bottiglia, ed è il riferimento che il resto dell'ecosistema usa per i propri benchmark. Un caso su tutti: nel 2025 il progetto gRPC-java ha benchmarkato pubblicamente la sua gestione Base64 basata su Guava contro java.util.Base64 (issue 11857), e l'implementazione del JDK è uscita approssimativamente 2.5 a 3.8 volte più veloce in codifica e 1.3 a 2.1 volte più veloce in decodifica su JDK 17 e 21, con le differenze maggiori su x86. È un indizio forte su dove è andato lo sforzo di implementazione del JDK, ed è la stessa conclusione che continuerai a trovare nei benchmark Base64: la versione della libreria standard è ora quella veloce, non quella legacy.
Due note pratiche. Prima, per file enormi è il profilo di memoria, non la velocità, ciò che stai gestendo, ed è per questo che esiste la sezione sullo streaming: wrap(InputStream) tiene l'insieme di lavoro al tuo buffer di lettura. Secondo, se finisci su un percorso caldo che decodifica milioni di piccoli valori, condividi un'istanza di decoder (la factory restituisce già la stessa istanza condivisa, come notato in cima), salta l'overload decode(String) quando hai già i byte (copia prima la stringa attraverso il Latin-1), e lascia che l'overload decode(byte[], byte[]) scriva in un array di destinazione già dimensionato per saltare la danza delle allocazioni.
Insidie con accento Java
Le trappole raccolte in un posto solo, tutte specifiche di Java:
- Decoder sbagliato per l'alfabeto. Una stringa base64url dentro
getDecoder()(o il contrario) è il classico erroreIllegal base64 character, di solito con un2d,5f,2bo2fnel messaggio. Abbina il decoder al protocollo, ogni volta. - Spazi bianchi di coda presi dal mondo. I valori copiati da un terminale, da una variabile d'ambiente o da un file di configurazione arrivano spesso con un a capo, e il decoder rigoroso lo trasforma in
Illegal base64 character a. Faistrip()dell'input, o usa il decoder MIME solo quando i dati sono davvero avvolti. - La trappola dell'armatura.
getMimeDecoder()non capisce gli header PEM, e le lettere di BEGIN e CERTIFICATE si decodificano come dati. Rimuovi le righe dell'armatura tu, sempre. - La tolleranza MIME come scorciatoia. Decodificare con il decoder MIME solo per "stare tranquilli" salta in silenzio qualsiasi carattere vagante non dell'alfabeto, quindi un payload corrotto può uscire plausibile e sbagliato. Usalo solo per input MIME veri.
- Charset lasciato al caso. Il
new String(bytes)senza argomenti usa il default della piattaforma. È UTF-8 su JDK 18+, ma il tuo codice dovrebbe passareStandardCharsets.UTF_8esplicitamente, o goditi il mojibake dopo la prossima migrazione del server. - Stringificare il binario.
new String(decodedPng)e tornare indietro è distruzione di dati: ogni sequenza di byte non valida nel tuo charset diventa il carattere di sostituzione, e il viaggio di andata e ritorno è a senso unico. Byte dentro, byte fuori, fino all'ultimo passo. - Fiducia nei bit di scarto.
MT==si decodifica proprio comeMQ==, quindi un payload con spazzatura nascosta nei bit non significativi passa ogni controllo che il JDK esegue. Se il protocollo conta, imponi la forma canonica. - Stream di JDK 8, 11 e 12. Il decoder avvolto su quelle versioni può aggiungere due byte zero vaganti per certe dimensioni di buffer (JDK 8222187, corretto in 16). Sul 16 e successivi non è un problema; su quelli vecchi sì.
- null non è vuoto. Passare
nulladecode()è unNullPointerException, non un array vuoto. Se una variabile può essere null, gestiscila prima della chiamata. - Android è un altro zoo. Su Android,
java.util.Base64esiste solo dal livello API 26; sotto, la classe del framework èandroid.util.Base64con le sue costanti flag. Un codice che fissa l'una o l'altra senza alcun controllo si rompe esattamente sui dispositivi che non hai mai testato. - Dimenticare che non è sicurezza. Il Base64 nasconde una password a un'occhiata e a nessun altro. Se i dati sono segreti, cifrali prima e solo dopo impacchettali se il canale chiede testo.
La lunga strada verso java.util.Base64
La storia del formato è più vecchia di Java. Negli anni '80 l'infrastruttura di posta di internet poteva portare solo ASCII a 7 bit, e chi voleva spostare binari inventava dialetti locali: uuencode per UNIX (il suo alfabeto attraversa codici ASCII consecutivi, quindi codificare era una sola addizione di 32 senza tabella di consultazione) e BinHex per le macchine Apple (che ha curato il suo alfabeto per buttare via caratteri visivamente confondibili come 7, O, g e o). Nel 1987 il protocollo Privacy-Enhanced Mail (RFC 989) ha standardizzato lo schema a 64 caratteri con righe da 64 caratteri per portare i certificati, e il RFC 1421 nel 1993 ha mantenuto l'alfabeto e le regole del padding. Nel 1996 il MIME (RFC 2045, che aggiorna il RFC 1521 del 1993) ha portato lo schema già chiamato "base64" dal suo alfabeto a 64 caratteri, ha fissato la lunghezza di riga da 76 caratteri che ancora avvolge i tuoi allegati email, e ha scritto la regola del decoder tollerante che getMimeDecoder() implementa ancora oggi. Nel 2003 il RFC 3548 ha provato a rimettere in ordine tutta la famiglia e ha dichiarato che i decoder dovrebbero rifiutare i caratteri fuori alfabeto, e nel 2006 il RFC 4648 è diventato lo standard che tutti citano, con le tabelle degli alfabeti, la variante base64url nella sezione 5 e la sezione sulla sicurezza che tiene onesta l'ultima delle sezioni di questo articolo.
Il capitolo di Java è un po' più drammatico. Per anni l'unico Base64 dentro il JDK era la coppia interna sun.misc.BASE64Encoder e sun.misc.BASE64Decoder, il tipo di API che compila oggi e svanisce senza un avviso di deprecazione, e se ti serviva Base64 in un mondo XML c'era anche javax.xml.bind.DatatypeConverter da JAXB. Tutti gli altri usavano Apache Commons Codec o Guava. Poi il 18 marzo 2014: Java 8 ha rilasciato java.util.Base64, che implementa RFC 4648 e RFC 2045 in una singola classe con il pattern dei metodi factory che hai usato fin dall'inizio. Tre anni e mezzo dopo, Java 9 (21 settembre 2017) ha rimosso per sempre la coppia sun.misc, e la guida di migrazione ufficiale è lapidaria: "In particolare, sun.misc.BASE64Encoder e sun.misc.BASE64Decoder sono stati rimossi. Invece, usa la classe supportata java.util.Base64, aggiunta nel JDK 8". Esegui jdeps su codice vecchio che li riferenzia ancora e lo strumento marca la dipendenza come "JDK removed internal API". Java 11 ha proseguito rimuovendo il modulo JAXB e il suo DatatypeConverter insieme (JEP 320). Dal 1.8 l'API pubblica non ha cambiato un singolo metodo, e il javadoc porta ancora il suo tag originale Since: 1.8. A muoversi è stato il motore sotto: correzioni di bug (il bug di stream JDK 8222187, corretto nel JDK 16) e lavoro sulle prestazioni, ed è per questo che i benchmark della comunità continuano a atterrare sulla stessa conclusione. Dodici anni, un'API, ed è ancora il Base64 più veloce che non dovrai pagare.
Fatti divertenti, edizione Java
Perché una guida completa dovrebbe finire con un sorriso, ecco alcuni fatti specifici di Java che sono semplicemente divertenti:
- Il javadoc Oracle di
decode(byte[] src, byte[] dst)promette che "alcuni byte potrebbero essere stati scritti nell'array di output prima che venga lanciato IllegalargumentException". Non IllegalArgumentException, IllegalargumentException, con la a minuscola. L'errore è nel codice sorgente reale del JDK, ed è lì dal 2014. Una documentazione così legata a un refuso è più rara di quanto dovrebbe. - Decodifica una stringa che contiene
ée il messaggio d'errore èIllegal base64 character -17: un numero esadecimale negativo, perché il carattere diventa il byte Latin-10xE9, che come byte Java con segno vale meno 23, e il JDK lo stampa in base 16. Il tuo log degli errori, per un momento, sta facendo aritmetica con segno. Base64.getDecoder() == Base64.getDecoder()è vero. Il codice sorgente restituisce un'istanza statica condivisa a ogni chiamata, quindi l'API "prendine una nuova" è un costume per un singleton, e la promessa di thread-safety è solo la descrizione di ciò che la JVM sta già facendo.- Dai in pasto al decoder URL una stringa di quattro trattini bassi,
"____", e restituisce tre byte di puro0xFF. Il trattino basso è il valore di alfabeto 63, quattro di questi fanno 24 bit, e 24 bit di uno sono la tripla di byteFF FF FF. Niente di illegale, ed è la parte più divertente. AA==si decodifica in un singolo byte NUL mentre la stringa vuota si decodifica nel nulla. Nel Base64, "niente" e "uno zero" sono creature diverse, e sono entrambe input perfettamente validi.- La piccola stringa
TWFuche si decodifica inManè diventata il test di fumo preferito dell'ecosistema: compare nel RFC, in Wikipedia, nei manuali di riferimento e nella maggior parte dei tutorial Base64 sulla terra, quindi ogni decoder scritto da allora ha pagato lo stesso piccolo tributo. - Ogni PNG codificato in Base64 che hai mai decodificato comincia con
iVBORw0K. È il numero magico del PNG in incognito, ed è uno dei prefissi a otto caratteri più riconoscibili di internet. - La sezione URL-safe del RFC 4648 è il posto dove nasce il nome "base64url": la spec dice che la codifica "può essere chiamata base64url" e avverte che "non dovrebbe essere considerata la stessa della codifica base64". L'origine dell'alfabeto URL-safe è citata in nota a un post del 2001 su una mailing list di hacker P2P, quindi il nome che incolli in ogni URL ha un pedigree da mailing list.
- Gli ID video di YouTube sono base64url senza padding, la familiare stringa di undici caratteri che puoi incollare ovunque in un URL. Il formato progettato per gli allegati email ora fa girare una piattaforma video, e
getUrlDecoder()è la parte del tuo JDK che lo rende possibile. - Decodifica la stringa
YmFzZTY0e ottieni di nuovo la parolabase64, nessun padding necessario, perché sei è un multiplo di tre. Un formato che si descrive da solo è l'equivalente tecnico di uno specchio che parla in Morse.
L'altra direzione
Questa è la parte decoder della storia, ed è là che vive gran parte del dolore, perché decodificare è il posto dove incontri i dati degli altri: le loro scelte di padding, i loro a capo, i loro charset, i loro token, la loro armatura. L'altra direzione, trasformare byte in una stringa Base64 con gli encoder di java.util.Base64, è un animale più calmo: non lancia mai su input non valido (non esiste input non valido da codificare), ha un conto di dimensioni da pagare invece di un messaggio d'errore da leggere, e il suo set di trappole (il passo del charset, le perille MIME, la decisione sul padding per i token) ha una guida tutta sua. La codifica Base64 in Java, collegata da questa pagina, copre l'encoder con la stessa profondità, e le due guide si leggono comodamente come una coppia.
Ultimo aggiornamento: 2026-09-08
Articolo correlato: Codifica Base64 in Java: una guida completa