最近在做一个前后端分离的项目,前端用JS,后端用PHP。本来以为就是个普通的CRUD,结果在字符转义这块栽了个大跟头。今天就把这个坑分享出来,顺便聊聊怎么在JS和PHP之间优雅地传递特殊字符。
先说说我遇到的场景:前端用JS生成了一段包含特殊字符的JSON,要通过AJAX传给PHP处理。看起来很简单对?结果PHP那边收到的数据总是莫名其妙少几个字符,或者多几个反斜杠。就像你精心准备的约会对象,见面时发现完全不是同一个人。
JS的escape/unescape已经被废弃了,现在主流用encodeURIComponent/decodeURIComponent。但是注意了,这俩函数处理空格时会变成加号(+),而PHP的urldecode默认会把加号解码为空格。这就导致了一个经典问题:
var data = "hello world";
var encoded = encodeURIComponent(data); // 结果是"hello+world"
// PHP收到后用urldecode解码,完美还原
// 但是如果data里本来就有加号?
var data = "1+1=2";
var encoded = encodeURIComponent(data); // 结果是"1%2B1%3D2"
// PHP解码后得到"1+1=2",看起来没问题
// 但是如果前端用escape?
var escaped = escape(data); // 结果是"1+1%3D2"
// PHP用urldecode解码后得到"1+1=2",似乎也没问题
// 但是当字符串里有Unicode字符时...
var data = "中文";
var escaped = escape(data); // "%u4E2D%u6587"
// PHP的urldecode根本不认识%u开头的编码
看到没?这就是为什么escape被废弃了。现代JS应该统一用encodeURIComponent,然后在PHP端用urldecode处理。但是等等,这还没完...
PHP那边也有自己的转义函数,比如htmlspecialchars和addslashes。htmlspecialchars会把<、>、"等字符转成HTML实体,防止XSS攻击。addslashes则是在特定字符前加反斜杠,主要用于数据库查询。这两个函数经常被滥用,比如:
$input = $_POST['data'];
$safe_input = addslashes(htmlspecialchars($input));
// 然后直接拼接到SQL查询里
$sql = "INSERT INTO table VALUES ('$safe_input')";
// 你以为这样安全了?太天真!
htmlspecialchars应该在输出到HTML时使用,而不是在存储数据时。其次,addslashes根本不能防止SQL注入,应该用预处理语句。正确的做法是:
// 存储原始数据
$stmt = $pdo->prepare("INSERT INTO table VALUES (?)");
$stmt->execute([$input]);
// 输出时再转义
echo htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
再来说说JSON。JS和PHP都有JSON解析器,但它们的实现细节有些差异。比如PHP的json_encode默认会把斜杠(/)转义成\/,而JS的JSON.parse能正确处理这种情况。但是如果你用字符串拼接的方式构造JSON,就可能出问题:
// JS端
var data = {
"message": ""
};
var json = JSON.stringify(data); // 正确结果是{"message":""}
// 但如果用字符串拼接
var badJson = '{"message":""}'; // 这会导致XSS漏洞
// 正确的做法是统一用JSON.stringify
PHP端也有类似问题:
$data = ["message" => ""];
$json = json_encode($data); // 正确结果{"message":"<\/script>"}
// 如果用字符串拼接
$badJson = '{"message":""}'; // 危险!
// 更糟的是有些开发者会这样做:
$badJson = "{'message':''}"; // 这不是合法的JSON
说到转义,不能不提正则表达式。JS和PHP的正则语法很像,但转义规则有差异。比如在PHP中匹配一个反斜杠需要写四个反斜杠:
// PHP
preg_match("/\\\\/", $string); // 匹配单个反斜杠
// 等价于JS的
/\\/.test(string); // 只需要两个反斜杠
这是因为PHP字符串本身需要转义,然后正则引擎又需要转义。这种多层转义经常把人搞晕。我的建议是:在PHP中用单引号字符串可以减少一层转义:
preg_match('/\\\/', $string); // 现在只需要三个反斜杠了
最后说说Base64编码,这经常被用来传递二进制数据。JS有btoa和atob,PHP有base64_encode和base64_decode。看起来很简单,但有个坑:JS的btoa不能直接处理Unicode字符串:
btoa(data); // 报错:字符串包含非Latin1字符
// 需要先转UTF-8
btoa(encodeURIComponent(data)); // 可行但结果很长
// 更好的方法是:
btoa(String.fromCharCode.apply(null,
new TextEncoder().encode(data))); // 需要现代浏览器支持
PHP那边相对简单:
$data = "中文";
$encoded = base64_encode($data); // 直接工作
// 但要注意解码时要确保原始编码一致
$decoded = base64_decode($encoded);
// 如果用在JSON中,记得去掉末尾的等号(=)
$forJson = rtrim($encoded, '=');
总结一下JS和PHP字符转义的最佳实践:
1. 前后端通信统一使用encodeURIComponent/decodeURIComponent
2. JSON处理永远用内置的JSON.stringify/parse和json_encode/json_decode
3. HTML转义只在输出时做,用htmlspecialchars
4. 数据库查询永远用预处理语句,不要手动转义
5. 正则表达式注意PHP的多层转义特性
6. Base64处理注意Unicode字符的问题
记住,转义就像穿衣服 - 脱的时候要和穿的时候顺序相反。如果你在某个环节多加了一层转义,后面就得记得去掉。最好的办法是尽量减少手动转义的次数,让标准库去做这些脏活。
最后的最后,如果你在凌晨三点调试字符转义问题,发现代码里全是反斜杠,眼睛已经花了...这时候最好的解决方案是:关掉IDE,去睡觉。明天早上用fresh eyes来看,问题往往一目了然。别问我怎么知道的。