编码和解码字符串
Dart 字符串内部是 UTF-16,所以编码前要先用 utf8.encode 转成 UTF-8 字节:
import 'dart:convert';
void main() {
final enc = base64Encode(utf8.encode('héllo wörld'));
print(enc); // aMOpbGxvIHfDtnJsZA==
print(utf8.decode(base64Decode(enc))); // héllo wörld
}
base64Encode(bytes) 是 base64.encode(bytes) 的简写,base64Decode(s) 是 base64.decode(s) 的简写。如果解码出的字节不是 UTF-8,utf8.decode 会抛出 FormatException: Invalid UTF-8 byte;传入 allowMalformed: true 则会用替换字符代替。
用同样这两个调用就能构造 HTTP Basic Auth 请求头,例如配合 package:http 使用:
final headers = {'Authorization': 'Basic ${base64Encode(utf8.encode('user:pass'))}'};
// Authorization: Basic dXNlcjpwYXNz
Basic Auth 请求头生成器 可以为任意凭据显示同样的值。
URL 安全的 Base64 与 normalize()
base64Url 是第二个编解码器,使用 -_ 字母表。两个编码器都总会写出填充,如果你的格式不需要填充,就把它去掉:
final d = utf8.encode('<<???>>');
print(base64Url.encode(d)); // PDw_Pz8-Pg==
print(base64Url.encode(d).replaceAll('=', '')); // PDw_Pz8-Pg
在解码方面,Dart 比大多数语言都更友好。两个解码器都接受两种字母表,但要求填充正确。normalize 能修复填充,还会转换字母表:
base64Url.normalize('PDw_Pz8-Pg'); // PDw_Pz8-Pg==
base64.normalize('PDw_Pz8-Pg'); // PDw/Pz8+Pg==
utf8.decode(base64Url.decode(base64Url.normalize('PDw_Pz8-Pg'))); // <<???>>
utf8.decode(base64Decode('PDw_Pz8-Pg==')); // <<???>>
如果输入不是有效的 Base64,normalize 同样会抛出 FormatException,所以它也可以当校验器用。JWT 的各段是不带填充的 Base64URL,见 Base64 与 Base64URL 的区别 和 填充详解,或者直接把令牌粘贴到 JWT 解码器 中。
有了 normalize,读取 JWT 的载荷只需要一行:
const jwt = 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0In0.sig';
final payload = utf8.decode(base64Url.decode(base64Url.normalize(jwt.split('.')[1])));
print(payload); // {"sub":"1234"}
print(jsonDecode(payload)['sub']); // 1234
这只是读取声明,并没有验证签名,所以绝不要在客户端依据这些声明做授权判断。
编码和解码文件
import 'dart:io';
final bytes = await File('hello.txt').readAsBytes();
print(base64Encode(bytes)); // aGVsbG8K
await File('copy.txt').writeAsBytes(base64Decode('aGVsbG8K'));
流式处理大文件
base64.encoder 和 base64.decoder 都是流转换器,因此文件可以逐块转换:
// encode: bytes -> Base64 text -> ASCII bytes on disk
await File('big.bin')
.openRead()
.transform(base64.encoder)
.transform(ascii.encoder)
.pipe(File('big.b64').openWrite());
// decode: ASCII bytes -> String chunks -> bytes
await File('big.b64')
.openRead()
.transform(ascii.decoder)
.transform(base64.decoder)
.pipe(File('big.out').openWrite());
转换器会在块与块之间保留剩余的字节,所以块大小无关紧要。无论哪个方向,一个 300 KB 的测试文件得到的结果都与内存中的函数逐字节相同。
Flutter:图片与 Data URI
显示 Base64 图片:
Image.memory(base64Decode(b64))
要从资源文件或用户选择的文件生成 Base64,读取字节后调用 base64Encode 即可。对于 Data URI,UriData 会处理前缀:
UriData.parse('data:text/plain;base64,aGk=').contentAsBytes(); // [104, 105]
UriData.fromBytes(pngBytes, mimeType: 'image/png').toString(); // data:image/png;base64,...
直接把 Data URI 传给 base64Decode 会报 Invalid character (at character 5),出错的是 data 后面的 :。想查看一个来历不明的字符串里装的是什么,可以用 Base64 转图片 在浏览器中预览。
校验与 FormatException 报错信息
所有解码错误都是 FormatException。toString() 包含错误信息、输入内容,以及一个指向问题位置的脱字符;e.message 和 e.offset 则分别给出这两部分。使用 base64Decode 时:
| 输入 | 报错信息 |
|---|---|
aGk | Invalid length, must be multiple of four (at character 4) |
a | Invalid length, must be multiple of four (at character 2) |
aGk=!! | Invalid padding character (at character 5) |
aGk== | Invalid padding character (at character 5) |
aGl= | Invalid encoding before padding (at character 4) |
"aGVs\nbG8K" | Invalid character (at character 5) |
在两点上,Dart 比许多语言都严格:它拒绝换行符(处理 MIME 或 PEM 输入时要先去掉),也拒绝不为零的填充位(aGl=)。一个有效性检查函数:
bool isBase64(String s) {
try {
base64Decode(s);
return true;
} on FormatException {
return false;
}
}
如果字符串解码失败,而报错信息又说明不了问题,Base64 校验工具 会标出问题所在;Base64 常见错误 讲解了各种原因。
常见陷阱
- 对
codeUnits编码:base64Encode('é'.codeUnits)返回6Q==(这是 Latin-1,不是 UTF-8),而'✓'.codeUnits会抛出ArgumentError: Invalid argument (Not a byte value at index 0: 0x2713)。请始终经过utf8.encode。 - 来自其他系统的换行符:电子邮件中的 MIME 输出或 Linux 上
base64的输出都包含\n;先用s.replaceAll(RegExp(r'\s'), '')去掉空白。 - 以为
base64Encode输出的是 URL 安全格式:它输出的是+和/。凡是要放进 URL 的内容,请用base64Url.encode。 - 没有 MIME 换行:这些编解码器从不插入换行符。需要的话,请自己按 76 个字符分行。
常见问题
如何在 Flutter 中显示 Base64 图片?
先解码成字节,再用 Image.memory(base64Decode(b64))。如果字符串是 Data URI,先把逗号及其之前的内容去掉,或者用 UriData.parse(uri).contentAsBytes()。
为什么 base64Decode 报 Invalid length, must be multiple of four?
输入没有填充,这在 JWT 和 URL 安全令牌中很常见。先调用 base64.normalize(s) 或 base64Url.normalize(s),它会补上缺失的 =。
Dart 的 base64Decode 能解码 URL 安全的输入吗?
能。解码器同时接受 +/ 和 -_,所以 base64Decode('PDw_Pz8-Pg==') 可以正常工作。只有编码器有区别:base64 输出 +/,base64Url 输出 -_。