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

PHP 中的 Base64

PHP 有两个函数 base64_encode 和 base64_decode,另外还有一个处理大文件的流过滤器。下面的所有输出均来自 PHP 8.4。

编码和解码字符串

PHP 字符串是字节串,所以 base64_encode 会对字符串里的任何字节进行编码。源文件以 UTF-8 保存时,不需要任何转换就能得到 UTF-8 的 Base64:

<?php
$enc = base64_encode("héllo wörld");
echo $enc;                  // aMOpbGxvIHfDtnJsZA==
echo base64_decode($enc);   // héllo wörld

如果文本来自使用其他字符集的数据库或表单,请先用 mb_convert_encoding($s, 'UTF-8', 'Windows-1252') 转换。否则得到的 Base64 对那些字节来说是对的,但在别处解码就会变成乱码。Base64 转文本 工具可以让你对结果尝试不同的字符集。

严格模式:base64_decode($s, true)

不传第二个参数时,base64_decode 会扔掉所有不在字母表中的字符,再解码剩下的部分。传入 true 时,它会改为返回 false:

var_dump(base64_decode("aGk=!!"));        // string(2) "hi"
var_dump(base64_decode("aGk=!!", true));  // bool(false)

一定要用 === 检查,因为有效的空输入会解码为 "",而它在布尔上下文中为假:

$bytes = base64_decode($input, true);
if ($bytes === false) {
    throw new InvalidArgumentException('Invalid Base64');
}

严格模式并没有名字听起来那么严格。以下结果在 PHP 8.4 上测试:

输入严格模式结果
aGk(无填充)"hi"
"aGk=\n"(结尾换行符)"hi"
aGl=(填充位不为零)"hi"
aGk==(填充过多)false
a(不可能的长度)false
PDw_Pz8-Pg==(URL 安全字符)false

如果只接受规范的 Base64,就先去掉空白,用严格模式解码,再检查 base64_encode($bytes) === $input。Base64 校验工具 会执行同样的检查,并指出有问题的字符。

带填充和不带填充的 URL 安全 Base64

PHP 没有内置的 Base64URL 函数。用 strtr 替换这两个字符,对于单字符映射,它比 str_replace 更快:

function b64url_encode(string $data, bool $pad = false): string {
    $s = strtr(base64_encode($data), '+/', '-_');
    return $pad ? $s : rtrim($s, '=');
}

function b64url_decode(string $s): string|false {
    return base64_decode(strtr($s, '-_', '+/'), true);
}

echo b64url_encode("<<???>>");        // PDw_Pz8-Pg
echo b64url_encode("<<???>>", true);  // PDw_Pz8-Pg==
echo b64url_decode("PDw_Pz8-Pg");     // <<???>>

由于严格模式能容忍缺失的填充,解码前无需把 = 补回去。两者区别的更多内容见 Base64 与 Base64URL 的区别。

编码和解码文件

echo base64_encode(file_get_contents("hello.txt"));   // aGVsbG8K

file_put_contents("copy.txt", base64_decode("aGVsbG8K", true));

要生成 Data URI,在前面加上 MIME 类型:'data:image/png;base64,' . base64_encode($png)。如果启用了 fileinfo,mime_content_type($path) 可以检测类型。

用流过滤器处理大文件

file_get_contents 会同时把整个文件和它的 Base64 副本加载进内存,内存占用约为文件大小的 2.3 倍。convert.base64-encode 流过滤器则边复制边编码:

$in  = fopen("big.bin", "rb");
$out = fopen("big.b64", "wb");
stream_filter_append($out, "convert.base64-encode");
stream_copy_to_stream($in, $out);
fclose($in);
fclose($out);

解码的方式相同,也可以通过 php://filter URL 一行搞定:

copy("php://filter/read=convert.base64-decode/resource=big.b64", "big.out");

在一个 300 KB 的测试文件上,两者的输出都与内存中的函数完全相同。手动循环也可以,前提是每块都是 3 字节的倍数,例如 fread($in, 3 * 8192)。任何其他大小都会在输出中间插入 =。

MIME 换行

邮件正文要求每行最多 76 个字符。chunk_split 正好做这件事,默认使用 \r\n 作为行尾:

echo chunk_split(base64_encode(str_repeat("x", 60)));
// eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4
// eHh4

它还会在末尾加一个 \r\n。邮件库会替你处理这些;MIME Base64 编码器 展示了这种格式。

常见陷阱

  • 对不可信输入忘了开启严格模式:错误的数据会被解码成随机字节,然后在离原因很远的地方出错。
  • 在默认模式下解码 URL 安全的输入:base64_decode("PDw_Pz8-Pg==") 返回 string(6) "<<���",而不是 <<???>>。请先用 strtr 转换。
  • 传入 null:在 PHP 8.1+ 中会得到 Deprecated: base64_encode(): Passing null to parameter #1 ($string) of type string is deprecated。传入数组则会抛出 TypeError: base64_encode(): Argument #1 ($string) must be of type string, array given。
  • + 变成空格:查询字符串中的标准 Base64 会在 URL 解码时被弄坏,因为在那里 + 表示空格。请使用 URL 安全变体或 rawurlencode。
  • 把它当作密码存储方案:Base64 什么都藏不住。密码请用 password_hash,另见 编码与加密的区别。

版本说明

  • base64_encode、base64_decode 和 $strict 参数在所有受支持的 PHP 版本中都存在(严格模式在 5.2 中加入)。
  • 从 PHP 8.0 起,base64_decode 的返回类型声明为 string|false,传入数组或对象会抛出 TypeError,而不再只是警告。
  • convert.base64-encode 过滤器接受 line-length 和 line-break-chars 选项来生成 MIME 输出,例如 stream_filter_append($out, 'convert.base64-encode', STREAM_FILTER_WRITE, ['line-length' => 76, 'line-break-chars' => "\r\n"])。

常见问题

base64_decode 的第二个参数有什么作用?

base64_decode($s, true) 开启严格模式:输入中只要有 Base64 字母表之外的字符,函数就返回 false。不开启时,这些字符会被悄悄丢弃。

PHP 有 base64url_encode 函数吗?

没有。编码用 rtrim(strtr(base64_encode($data), '+/', '-_'), '='),解码用 base64_decode(strtr($s, '-_', '+/'), true)。严格模式能接受缺失的填充。

base64_decode 的严格模式算是完整的校验吗?

不算。它仍然接受空白、缺失的填充以及 aGl= 这类不为零的填充位。要精确校验,就把结果重新编码,再与规范化后的输入比较。

为什么 base64_decode 返回乱码而不是 false?

你多半调用时没有传 true。在默认模式下,base64_decode('PDw_Pz8-Pg==') 会丢掉 _ 和 -,返回六个错误的字节。请先用 strtr 转换 URL 安全的输入。

继续阅读