首页 >  文章 >  php教程

PHP 8.5 URI 扩展怎么替代 parse_url:规范化、相对路径与请求校验

来源:17golang原创

时间:2026-08-16 13:44:50 323浏览 收藏

做回调地址校验的时候很多人容易踩坑:代码里随手调用函数拆出host和path,看似已经把外部地址拆解完成,真遇到大小写差异、默认端口省略、相对路径跳转或者多余点段这类边界输入时,最终校验结果很可能和实际请求指向完全对不上。PHP 8.5 新增的URI扩展直接把RFC 3986和WHATWG URL两套标准的处理逻辑封装成了清晰的类,完全可以帮你把过去硬拆字符串的写法,升级成「先做规范化,再走业务判断」的稳定流程。

实践要点
  • parse_url() 仍适合快速读取组件,但它返回数组,不能替代完整的 URI 对象语义。
  • Uri\\Rfc3986\\Uri 会提供规范化后的 host、path、query,并能用 resolve() 合并相对引用。
  • 请求白名单应比较规范化后的 scheme、host、port 和 path,不能只看字符串前缀。
  • 项目升级到 PHP 8.5 前,先确认运行环境版本,再用旧输入样本回归异常 URI 和相对路径。

一个回调地址校验,为什么在边界输入上失去一致性

假设订单系统只允许把通知发到 hooks.example.com,旧代码通常会这样写:

$parts = parse_url($callback);

if (($parts['host'] ?? '') !== 'hooks.example.com') {
    throw new InvalidArgumentException('回调域名不在白名单');
}

$path = $parts['path'] ?? '/notify';

这段代码并非不能用。问题在于它把 URI 当成一组可选数组字段,后续代码还要自己决定空路径、默认端口、相对引用和编码字符怎么处理。不同调用点各写一套规则后,校验和真正发请求的结果就可能不一致。

PHP 官方文档把 parse_url() 定位为提取 URL 组件的函数;PHP 8.5 的 URI 扩展则提供遵循 RFC 3986 和 WHATWG URL 标准的对象 API。两者不是简单的新旧函数替换关系,先确认业务需要哪种标准,再决定迁移范围。

PHP URI 规范化流程:原始回调地址经过 Uri 对象处理后进入 host、path 和 query 校验

先看 parse_url 和 Uri 对象分别解决什么问题

快速读取几个字段时,parse_url() 依然直观:

$parts = parse_url('https://hooks.example.com:443/v1/../notify?order=42');

var_dump($parts['host'] ?? null);
var_dump($parts['path'] ?? null);

如果需要把 URI 继续传给别的层,PHP 8.5 可以使用 RFC 3986 实现:

use Uri\Rfc3986\Uri;

$uri = new Uri('https://hooks.example.com:443/v1/../notify?order=42');

echo $uri->getScheme(), PHP_EOL;
echo $uri->getHost(), PHP_EOL;
echo $uri->getPort(), PHP_EOL;
echo $uri->getPath(), PHP_EOL;
echo $uri->getQuery(), PHP_EOL;
echo $uri->toString(), PHP_EOL;

这里的关键不是多了几个方法,而是对象会保留 URI 的组成关系,并提供规范化后的读取结果。需要保留输入原貌时,可以使用对应的 getRaw* 方法;需要生成新的 URI 时,则使用 withPath()withQuery() 等返回新对象的方法。

用规范化结果做白名单判断,顺序比写法更重要

白名单判断建议拆成四个明确的条件:scheme、host、port 和 path。不要直接用 starts_with 思路判断整个字符串,也不要把用户名信息当作域名的一部分。

use Uri\Rfc3986\Uri;

function assertCallbackUri(string $value): Uri
{
    try {
        $uri = new Uri($value);
    } catch (\Uri\InvalidUriException $e) {
        throw new InvalidArgumentException('回调地址格式无效', 0, $e);
    }

    $port = $uri->getPort();
    $portAllowed = $port === null || $port === 443;

    if ($uri->getScheme() !== 'https'
        || $uri->getHost() !== 'hooks.example.com'
        || !$portAllowed
        || !str_starts_with($uri->getPath(), '/notify')) {
        throw new InvalidArgumentException('回调地址不在允许范围');
    }

    return $uri;
}

示例里的 str_starts_with() 只用于已经拿到的 path,并不负责判断整个地址是否可信。真实项目还要明确是否允许子路径、是否禁止 userinfo、是否需要排除 fragment,以及端口为空时是否按默认端口处理。判断规则写在一个边界函数里,比散落在控制器、队列消费者和 HTTP 客户端里更容易回归。

相对路径合并:resolve 比手工拼接更稳

分页接口、静态资源或回调响应里经常出现相对引用。手工拼接容易留下双斜杠、父级目录或查询串覆盖顺序的问题。PHP 8.5 的 URI 类可以把基准 URI 和相对引用分开表达:

use Uri\Rfc3986\Uri;

$base = new Uri('https://api.example.com/v1/orders/');
$next = $base->resolve(new Uri('../orders?page=2'));

echo $next->toString();
// https://api.example.com/v1/orders?page=2

这里别急着把 resolve() 当成网络请求。它只负责 URI 引用合并,不会替你发起访问,也不会判断目标域名是否安全。合并后仍应重新执行 scheme、host、port 和 path 的白名单检查。

PHP Uri resolve 合并相对路径后重新进入域名白名单检查,异常引用在边界处被拦截

修改 query 时,保留语义而不是拼接字符串

给分页链接追加参数时,直接在原字符串后面加问号并不可靠:原地址可能已经有 query,也可能带 fragment。URI 对象的 withQuery() 让“修改 query”成为一个独立动作。

$uri = new Uri('https://api.example.com/orders?sort=created_at#top');
$pageTwo = $uri->withQuery('sort=created_at&page=2');

echo $pageTwo->toString();
// https://api.example.com/orders?sort=created_at&page=2#top

如果 query 来自用户输入,仍要使用项目统一的参数编码策略,并在业务层限制允许的键和值。withQuery() 负责替换 URI 组件,不会自动把任意参数变成安全的筛选条件。

PHP 8.5 升级前,先把兼容边界测出来

URI 扩展属于 PHP 8.5 的新能力,旧版本运行时不能直接加载这些类。迁移时可以保留原来的适配器,让业务代码只依赖自己的接口:

interface CallbackAddress
{
    public function host(): ?string;
    public function path(): string;
    public function normalized(): string;
}

// PHP 8.5 适配器内部使用 Uri\Rfc3986\Uri;旧环境继续使用已有解析实现。
// 业务层只调用 CallbackAddress,不在控制器里判断 PHP 版本。

回归样本至少覆盖完整 HTTPS 地址、无端口地址、显式 443、非默认端口、相对引用、父级路径、空 query、带 fragment 的地址,以及非法字符。每个样本同时记录旧实现结果和新实现结果,只有差异能被解释时才进入切换。

检查项需要确认的结果不通过时的处理
运行时生产 PHP 版本达到 8.5,扩展类可加载先保留适配器,不直接切换业务路径
标准明确使用 RFC 3986 还是 WHATWG URL 语义不要让不同模块各自选择解析方式
白名单规范化后重新检查域名、端口和路径拒绝无法解释的差异输入
回归旧地址、新地址和异常地址结果可对照保留样本并补测试,不用线上请求试错

常见问题

PHP 8.5 的 Uri\Rfc3986\Uri 能完全替代 parse_url() 吗?

不能简单说完全替代。只取几个字段时,parse_url() 足够直接;需要规范化、相对引用合并和不可变修改时,URI 对象更合适。

RFC 3986 和 WHATWG URL 应该选哪个?

服务端 URI、API 路径和协议级资源引用通常先评估 RFC 3986;如果业务语义必须贴近浏览器 URL 处理,再评估 WHATWG 实现。不要只因为类名更新就切换标准。

resolve() 会检查目标地址是否安全么?

不会。它只合并基准 URI 和相对引用,合并后的结果仍要经过应用自己的域名、端口、路径和协议白名单。

旧 PHP 版本能安装 URI 扩展后继续使用这些类吗?

PHP 官方手册把这些 URI 类标为 PHP 8.5.0 起可用。旧环境应继续使用兼容实现,并把新类封装在版本适配层中。

把解析、规范化和业务校验分成三层

parse_url() 不是必须立刻删除的旧代码,PHP 8.5 URI 扩展也不是把所有地址处理都改成对象的理由。更稳的落地方式是:第一层负责解析,第二层负责规范化和相对引用合并,第三层负责白名单与业务规则。这样升级带来的差异有地方记录,异常输入有固定样本,真正的请求发送也不会被 URI 处理细节绑住。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>