中文域名转码有多坑?踩过这些雷的开发者都哭了

最近在做一个国际化的项目,客户非要支持中文域名访问。我当时就笑了,这年头谁还用中文域名?结果客户甩过来一个案例:某个政府网站因为不支持中文域名被投诉了...好,你赢了。于是开启了我的中文域名转码踩坑之旅。

先说说中文域名转码是啥东西。简单说就是把"例子.测试"变成"xn--fsq.xn--0zwm56d"这种鬼样子。专业术语叫Punycode编码,RFC3492标准定义的。PHP里有现成的函数idn_to_ascii和idn_to_utf8,听起来很美好是不是?Too young!

第一个坑:PHP版本问题

在PHP 7.2之前,这俩函数需要安装intl扩展。我司某个祖传服务器跑的是PHP 5.6,结果发现这函数压根不存在。当时我的表情就像看到同事用记事本写代码一样精彩。解决方案是手动实现,或者用第三方库。我选择了后者,用了true/punycode这个包:

composer require true/punycode

然后代码就得写成这样:

$punycode = new TrueBV\Punycode();

$ascii = $punycode->encode('例子.测试');

第二个坑:顶级域名处理

你以为转码完就完了?太天真了!有些顶级域名本身就是Punycode的,比如".中国"是".xn--fiqs8s"。这时候如果你把整个域名丢进去转码,结果会很魔幻:

idn_to_ascii('百度.中国'); // 可能给你转成"xn--wxtr44c.xn--fiqs8s"

// 但实际应该是"xn--wxtr44c.xn--fiqs8s"

看到没?看起来一样?仔细看,第一个点后面的部分被重复编码了。正确的做法是先分割域名:

$parts = explode('.', '百度.中国');

$encoded = array_map('idn_to_ascii', $parts);

$result = implode('.', $encoded);

第三个坑:特殊字符处理

有些中文域名里会混着英文、数字甚至符号。比如"腾讯-微信.公司"。这时候转码可能出各种幺蛾子。我遇到最离谱的一个case是:

idn_to_ascii('a&b.测试'); // 直接返回false

// 因为&在域名里是非法的

解决方案是先做合法性校验:

if(!preg_match('/^[a-z0-9\x80-\xff\-\.]+$/i', $domain)){

throw new Exception('非法字符');

}

第四个坑:大小写敏感

Punycode编码后都是小写,但有些场景下我们需要保留原始大小写。比如在做域名比对时:

$domain1 = 'Example.测试';

// 转码后都是"example.xn--0zwm56d",但业务上可能认为这是两个不同域名

这时候就得自己实现一个带大小写记忆的转码函数:

function my_idn_to_ascii($domain){

$parts = explode('.', $domain);

$result = [];

foreach($parts as $part){

if(preg_match('/[^a-z0-9\-]/i', $part)){

$result[] = strtolower(idn_to_ascii($part));

}else{

$result[] = $part; // 保留原始大小写

}

}

return implode('.', $result);

}

在处理批量域名时,我发现idn_to_ascii的性能简直感人。测试10万个域名转码要20多秒。后来发现是因为每次调用都会初始化编码器。解决方案是改用批量处理:

// 错误示范

foreach($domains as $domain){

$encoded[] = idn_to_ascii($domain);

}

// 正确姿势

$encoded = array_map('idn_to_ascii', $domains);

性能直接提升5倍,果然函数调用开销不是闹着玩的。

第六个坑:反向解码

你以为编码完就完事了?用户访问时还得把Punycode转回中文。这时候idn_to_utf8就派上用场了,但同样有坑:

// 用户访问的是xn--wxtr44c.xn--fiqs8s

$_SERVER['HTTP_HOST']; // 可能已经被服务器自动转码成"百度.中国"

这时候如果你再转码一次,就会得到乱码。所以要先判断:

if(strpos($host, 'xn--') !== false){

$host = idn_to_utf8($host);

}

第七个坑:邮件地址处理

项目里还有个需求要处理带中文域名的邮箱,比如"张三@例子.测试"。这时候不能简单用转码函数,因为@符号前后的处理逻辑不同:

$email = '张三@例子.测试';

list($local, $domain) = explode('@', $email);

$encoded = $local . '@' . idn_to_ascii($domain);

但这样会破坏原始邮箱的显示效果。更好的做法是保留两种形式:

$email_utf8 = $local . '@' . $domain;

第八个坑:URL处理

当中文域名出现在URL中时,情况更复杂。比如:

https://例子.测试/路径?参数=值

需要处理以下几个部分:

1. 协议头(http/https)

2. 域名部分

3. 路径和查询参数

我的解决方案是用parse_url拆解:

$url = 'https://例子.测试/路径?参数=值';

$parts = parse_url($url);

$parts['host'] = idn_to_ascii($parts['host']);

$newUrl = $parts['scheme'] . '://' . $parts['host'] . $parts['path'];

if(isset($parts['query'])){

$newUrl .= '?' . $parts['query'];

}

第九个坑:浏览器兼容性

你以为代码写对就完事了?不同浏览器对中文域名的处理方式不一样。Chrome会在地址栏显示中文,但实际请求发送Punycode;IE某些版本会直接显示Punycode;Safari会有时显示中文有时显示编码...

解决方案是在服务端做重定向:

if(strpos($_SERVER['HTTP_HOST'], 'xn--') === 0){

$utf8 = idn_to_utf8($_SERVER['HTTP_HOST']);

header("Location: https://{$utf8}{$_SERVER['REQUEST_URI']}");

exit;

}

第十个坑:SSL证书

最后这个坑最要命。申请SSL证书时,有些CA不支持直接对中文域名签发,必须用Punycode形式申请。但证书部署后,浏览器又期望看到中文形式...简直精神分裂。

解决方案是:

1. 用Punycode申请证书

2. 在证书的SAN(Subject Alternative Name)里同时包含中文和Punycode

3. 配置服务器支持两种形式的域名

折腾完这一大圈,我终于明白为什么没人爱用中文域名了。不过话说回来,这些坑踩过之后,现在处理国际化域名简直手到擒来。最后分享一个自用的完整工具类:

class IdnHelper {

public static function toAscii($domain) {

if(!function_exists('idn_to_ascii')){

throw new Exception('需要intl扩展或PHP>=7.2');

$parts = explode('.', $domain);

$encoded = [];

foreach($parts as $part){

if(preg_match('/[^\x20-\x7f]/', $part)){

$converted = idn_to_ascii($part, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46);

if($converted === false){

throw new Exception("转码失败: {$part}");

}

$encoded[] = $converted;

}else{

return implode('.', $encoded);

}

public static function inUrl($url) {

$parts = parse_url($url);

if(isset($parts['host'])){

$parts['host'] = self::toAscii($parts['host']);

$newUrl = '';

$newUrl .= $parts['scheme'] . '://';

$newUrl .= $parts['host'];

$newUrl .= '?' . $parts['query'];

return $newUrl;

}

}

用的时候就这么简单:

$ascii = IdnHelper::toAscii('例子.测试');

$url = IdnHelper::inUrl('https://例子.测试/api');

记住,中文域名就像榴莲,爱的人爱死,恨的人恨死。但作为一个专业的开发者,我们没得选,只能硬着头皮上。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值