上传文件
  1. base64hub
  2. 在代码中使用 Base64
  3. Dart
代码

Dart 中的 Base64

Dart 的 dart:convert 库为两种字母表都提供了 Base64 编解码器,还有一个可以修复填充的 normalize 方法。下面的输出均来自 Dart 3.13。

编码和解码字符串

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 时:

输入报错信息
aGkInvalid length, must be multiple of four (at character 4)
aInvalid 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 输出 -_。

继续阅读