
前言base64_encode()大概是 PHP 里最容易被误解的函数。它的名字里有 encode输出又是一串看不懂的字母数字很多人由此产生两个错误直觉一是编码过了就等于加密了二是编码过了就可以随便放进任何地方。这两个直觉都会出事。第一个直觉导致有人把密码、身份证号base64_encode之后存进数据库或写进日志以为已经脱敏。第二个直觉导致 base64 串直接拼进 URL 之后对不上——因为标准 Base64 的字符集里有、/、这三个字符在 URL、文件名、HTTP 头里的语义各不相同。base64_encode()的本质非常朴素把任意二进制数据按每 3 个字节一组映射成 4 个可打印 ASCII 字符。它只保证数据在只支持文本的通道里不会因为控制字符而损坏不保证机密性也不保证完整性更不保证在 URL 里能原样回来。本文分四部分先讲清楚 base64 的边界再按 URL、HTTP 头、JSON、表单文件、加密载荷五种真实传输场景给出正确写法然后讲大文件流式处理最后列坑。一、base64 做了什么没做什么1.1 只有编码没有加密Base64 是可逆的裸映射没有密钥、没有随机数、没有散列。任何人拿到字符串都能还原?php // base64-false-safety.php —— PHP 7.0 declare(strict_types1); $secret password123456; $encoded base64_encode($secret); echo $encoded, PHP_EOL; echo base64_decode($encoded), PHP_EOL;输出cGFzc3dvcmQ9MTIzNDU2 password123456一行base64_decode就还原了。需要保密就必须先加密再编码需要存密码就用password_hash()。1.2 体积会涨三分之一因为 3 字节变 4 字符Base64 的固定开销是 33.3%再加上填充符短数据的开销更夸张原始字节数Base64 字符数实际膨胀14300%3433.3%101660%1000133633.6%中文场景更要注意UTF-8 下一个汉字占 3 字节base64_encode(测试)的长度是 8而源串的strlen()已经是 6。这部分膨胀会直接冲击 URL 和 HTTP 头的长度限制。1.3 三种变体的区别变体使用的字符集填充典型场景标准 Base64A-Z a-z 0-9 /PEM、邮件正文、JSON 内部字段URL 安全 Base64A-Z a-z 0-9 - _通常省略URL 路径、查询串、文件名、JWTMIME 换行 Base64同标准每 76 字符插入\r\n邮件附件、证书文件关键差异就是那三个特殊字符在 URL 查询串里会被解码成空格/在 URL 路径里会被当成层级分隔符在部分场景会被截断。只要数据要经过 URL就应该用 URL 安全变体。二、五种传输场景的正确写法2.1 写一套 base64url 工具函数PHP 没有内置的base64url_encode()标准做法是用strtr()换字符、用rtrim()去填充。解码时要先把填充补回来?php // base64url.php —— PHP 7.0 declare(strict_types1); function base64url_encode(string $data): string { // 标准编码后把 / 换成 -_再去掉尾部填充 return rtrim(strtr(base64_encode($data), /, -_), ); } function base64url_decode(string $data): string { // 去掉可能存在的空白再还原字符集 $normalized strtr($data, -_, /); // 补回 填充使长度成为 4 的倍数 $remainder strlen($normalized) % 4; if ($remainder 1) { throw new InvalidArgumentException(不是合法的 base64url 字符串); } if ($remainder ! 0) { $normalized . str_repeat(, 4 - $remainder); } $decoded base64_decode($normalized, true); if ($decoded false) { throw new InvalidArgumentException(base64url 解码失败); } return $decoded; } $raw random_bytes(16); $url base64url_encode($raw); echo base64url : , $url, PHP_EOL; echo base64 : , base64_encode($raw), PHP_EOL; var_dump(base64url_decode($url) $raw);输出形如base64url : 8sK3vQ1xY9pLmZ0aWcTu7A base64 : 8sK3vQ1xY9pLmZ0aWcTu7A bool(true)注意%4 1这个分支合法的 base64 长度余数只可能是 0、2、3余 1 说明输入被截断或者根本不是 base64直接拒绝比让它解出垃圾数据要好。2.2 放进 URL 参数即使是 base64url放进查询串时也建议走一遍rawurlencode()避免和其他参数拼接时污染?php // url-transfer.php —— PHP 7.0 declare(strict_types1); require __DIR__ . /base64url.php; $payload json_encode([uid 42, exp time() 300], JSON_UNESCAPED_UNICODE); $token base64url_encode($payload); $url https://api.test/verify?t . rawurlencode($token); echo $url, PHP_EOL; // 模拟服务端接收$_GET 已经做了解码 parse_str(parse_url($url, PHP_URL_QUERY) ?? , $query); var_dump(base64url_decode($query[t]) $payload);这里有个必须理解的点parse_str()和$_GET会把解析成空格。如果这里用的是标准base64_encode()且没做 urlencode只要编码结果里恰好出现服务端就会拿到一个被破坏的字符串base64_decode()解出来的数据莫名其妙少几个字节——而且不报错。2.3 放进 HTTP 头或 JSON 字段HTTP 头字段的值必须是单行不能包含 CR 或 LF。Base64 本身不含换行所以千万不要在放进头之前调用chunk_split()那是给邮件正文和 PEM 用的?php // header-and-json.php —— PHP 7.0 declare(strict_types1); require __DIR__ . /base64url.php; $binary random_bytes(12); // 正确单行、URL 安全字符集 header(X-Payload: . base64url_encode($binary)); // 错误示范下面这种做法会把 \r\n 注入响应头导致头被截断或直接拒绝 // $bad chunk_split(base64_encode($binary), 76, \r\n); // header(X-Payload: . $bad); $response json_encode([ iv base64url_encode($binary), data base64url_encode(str_repeat(\x00\x01\x02, 4)), ], JSON_THROW_ON_ERROR); echo $response, PHP_EOL;JSON 里用标准 base64 也能过因为、/、在 JSON 字符串里没有特殊含义。但如果这份 JSON 之后还要作为 URL 参数传输提前用 base64url 可以少一次转码。2.4 当作文件名或路径段标准 Base64 里带/直接当文件名会变成目录层级在部分 shell 和服务器上也要转义。所以文件名一律用 base64url并且记得去掉?php // filename.php —— PHP 7.0 declare(strict_types1); require __DIR__ . /base64url.php; $id random_bytes(9); $name base64url_encode($id) . .bin; // 二次校验拒绝任何可能逃逸目录的字符 if (preg_match(/^[A-Za-z0-9_-]$/, pathinfo($name, PATHINFO_FILENAME)) ! 1) { throw new RuntimeException(文件名不合法); } $path sys_get_temp_dir() . DIRECTORY_SEPARATOR . $name; file_put_contents($path, $id); echo $path, PHP_EOL; echo base64url_decode(pathinfo($name, PATHINFO_FILENAME)) $id ? round-trip ok : mismatch, PHP_EOL;2.5 需要保密时的正确姿势先加密再编码正确的顺序永远是加密 → 编码解密时解码 → 解密。用带认证的加密模式AEADAuthenticated Encryption with Associated Data比如 AES-256-GCM这样密文被篡改时解密会直接失败而不是吐出一段垃圾?php // encrypt-then-encode.php —— PHP 7.1AES-GCM 支持 declare(strict_types1); require __DIR__ . /base64url.php; const CIPHER aes-256-gcm; function seal(string $plain, string $key): string { $ivLen openssl_cipher_iv_length(CIPHER); $iv random_bytes($ivLen); $tag ; $cipherText openssl_encrypt( $plain, CIPHER, $key, OPENSSL_RAW_DATA, $iv, $tag, , // 附加认证数据 AAD不需要就传空串 16 // 认证标签长度 ); if ($cipherText false) { throw new RuntimeException(加密失败); } // 把 iv 和 tag 一起打包它们是解密必需品 return base64url_encode($iv . $tag . $cipherText); } function open(string $token, string $key): string { $blob base64url_decode($token); $ivLen openssl_cipher_iv_length(CIPHER); if (strlen($blob) $ivLen 16) { throw new RuntimeException(密文长度不足token 被截断); } $iv substr($blob, 0, $ivLen); $tag substr($blob, $ivLen, 16); $cipher substr($blob, $ivLen 16); $plain openssl_decrypt($cipher, CIPHER, $key, OPENSSL_RAW_DATA, $iv, $tag); if ($plain false) { // tag 校验失败说明密文被改过 throw new RuntimeException(解密失败数据被篡改或密钥不对); } return $plain; } $key random_bytes(32); // AES-256 需要 32 字节密钥 $token seal(user_id42;roleadmin, $key); echo token : , $token, PHP_EOL; echo plain : , open($token, $key), PHP_EOL; // 篡改任意一个字符解密必然失败 $tampered substr($token, 0, -1) . ($token[-1] A ? B : A); try { open($tampered, $key); echo 不该走到这里, PHP_EOL; } catch (RuntimeException $e) { echo tamper detected: , $e-getMessage(), PHP_EOL; }密钥用random_bytes(32)生成后放进环境变量或密钥管理服务不要写死在代码里。openssl_cipher_iv_length()返回的 IV 长度由具体算法决定AES-GCM 通常是 12 字节用函数取而不是硬编码更稳妥。如果只是要保证数据没被改过而不需要保密用 HMAC 就够了注意比较时要用hash_equals()做恒定时间比较?php // hmac-token.php —— PHP 7.0 declare(strict_types1); require __DIR__ . /base64url.php; function sign(string $payload, string $key): string { $mac hash_hmac(sha256, $payload, $key, true); return base64url_encode($payload) . . . base64url_encode($mac); } function verify(string $token, string $key): string { $parts explode(., $token, 2); if (count($parts) ! 2) { throw new InvalidArgumentException(token 格式错误); } [$payload, $mac] $parts; $raw base64url_decode($payload); $given base64url_decode($mac); $want hash_hmac(sha256, $raw, $key, true); // 必须用 hash_equals用 会因为提前返回而泄漏时序信息 if (!hash_equals($want, $given)) { throw new RuntimeException(签名不匹配); } return $raw; } $key random_bytes(32); $token sign(order_id1001, $key); echo verify($token, $key), PHP_EOL;三、大文件的流式处理base64_encode(file_get_contents($path))会先把整个文件读进内存再生成一份 1.33 倍大的字符串。处理 200MB 的文件峰值内存轻松超过 500MB。一个可行的替代方案是用流过滤器PHP 内置了convert.base64-encode?php // stream-encode.php —— PHP 7.0 declare(strict_types1); $srcPath __DIR__ . /big.bin; if (!is_file($srcPath)) { file_put_contents($srcPath, random_bytes(1024 * 100)); } $out fopen(php://temp, wb); stream_filter_append($out, convert.base64-encode); $src fopen($srcPath, rb); stream_copy_to_stream($src, $out); fclose($src); rewind($out); echo 编码后长度: , strlen(stream_get_contents($out)), PHP_EOL; fclose($out);需要注意反向的convert.base64-decode过滤器不会忽略空白字符输入里只要混了换行就会失败。用之前先把空白清掉。如果要分块拼接比如边读边写进数据库的TEXT字段每块的原始字节数必须是 3 的倍数这样每块能独立编码且不产生填充直接字符串拼接即可正确还原?php // chunked-encode.php —— PHP 7.0 declare(strict_types1); function base64_encode_stream(string $path, int $chunkBytes 3 * 1024): string { if ($chunkBytes % 3 ! 0) { throw new InvalidArgumentException(分块大小必须是 3 的倍数); } $handle fopen($path, rb); if ($handle false) { throw new RuntimeException(无法打开文件: {$path}); } $result ; try { while (!feof($handle)) { $chunk fread($handle, $chunkBytes); if ($chunk false || $chunk ) { break; } $result . base64_encode($chunk); } } finally { fclose($handle); } return $result; } $f __DIR__ . /sample.bin; file_put_contents($f, random_bytes(10000)); $chunked base64_encode_stream($f); $oneShot base64_encode(file_get_contents($f)); echo $chunked $oneShot ? 两种方式结果一致 : 结果不一致, PHP_EOL;常见坑点❌ 认为 base64 编码后数据就看不清了用它存密码或脱敏身份证号 ✅ Base64 是公开可逆的任何人都能解存密码用password_hash()脱敏用哈希加盐或直接掩码❌ 把标准base64_encode()的结果直接拼进 URL 查询串 ✅会被parse_str()和$_GET解析成空格必须换成 base64url 字符集或至少套一层rawurlencode()❌ 把标准 base64 结果当作 URL 路径段或文件名 ✅/会打断路由和目录层级文件名和路径段一律用 base64url❌ 用base64_decode($str)不带第二个参数 ✅ 默认模式会静默丢弃所有非法字符输入被破坏时不报错、只解出垃圾传true开启严格模式失败返回false❌ 生成 PEM 或邮件附件时用chunk_split()插入换行然后把结果放进 HTTP 头 ✅ HTTP 头必须是单行带\r\n的值会导致头被截断甚至响应头注入chunk_split()只用在邮件正文和 PEM 文件里❌ 用convert.base64-decode流过滤器读带换行的 Base64 文本 ✅ 这个过滤器不忽略空白遇到\r\n就会失败先preg_replace(/\s/, , $data)再过滤❌ 对几百 MB 的文件直接base64_encode(file_get_contents($path))✅ 峰值内存是文件本身的 2.3 倍以上应该用convert.base64-encode流过滤器或者按 3 的倍数分块处理❌ 用比较 base64 编码后的 HMAC 签名 ✅ 字符串比较会在第一个不同字符处提前返回泄漏时序信息签名、令牌一律用hash_equals()❌ 传输加密数据时只做base64_encode没有认证明文完整性 ✅ 用 AES-256-GCM 这类 AEAD 模式密文被改一个字符解密即失败单纯 base64 的密文被篡改后往往解出一段乱码而无人察觉总结场景推荐做法必须避开的做法URL 查询串 / 路径base64url 编码必要时再rawurlencode标准 base64 直接拼文件名base64url 且去掉再做白名单校验用带/的标准 base64HTTP 头 / JSON单行 base64url绝不含\r\nchunk_split()后塞进头邮件 / PEM标准 base64 每 76 字符换行单行超长字符串需要保密openssl_encryptAES-256-GCM 后 base64只 base64 当加密需要防篡改HMAC-SHA256 hash_equals()比较用比较签名大文件convert.base64-encode流过滤器或 3 的倍数分块一次性读入内存Base64 的角色定位只有一句话它是二进制数据通过文本通道时的防损坏包装不是安全机制。把安全性交给加密和 HMAC把正确性交给字符集选择和严格模式解码base64_encode()这个函数本身几乎不会再给你制造麻烦。