跳到主内容
d.devtul.fun
EN
HTTP · 2026-09-01

URL 和 URI 到底有什么区别

日常对话里你说"URL",规范文档里写"URI",很容易以为这两个词说的是同一个东西。其实不完全一样,但这个差别远不像某些较真的人说的那么重要。这里给一个实话实说的版本,并补上那些真正会改变你写代码方式的细节。

URI 是超集

URI(统一资源标识符)是那个宽泛的大类:只要是用来标识一个资源的字符串,都算 URI。它下面有两个你最熟悉的分支:

  • URL(统一资源定位符)—— 不仅标识资源,还告诉你怎么找到它(带 http、https 之类的协议)。这就是地址栏里的"网址"。
  • URN(统一资源名称)—— 用持久标识符来命名资源,但不暗示位置,比如 urn:isbn:0451450523。

所以每个 URL 都是 URI,但不是每个 URI 都是 URL。URN 是 URI 里既不算 URL、也不做定位的那一支。用韦恩图说:URI 是外圈,URL 和 URN 是里面两个互不重叠的圆。

URN 在实践中

一个 URN 形如 urn:<命名空间>:<值>。你在书的 ISBN、urn:uuid:... 这类标识符,以及某些 XML、SOAP 工具里会遇到它。关键在于 URN 是"名字"而不是"地址":知道 URN 只告诉你这个东西是什么,不告诉你在哪能取到它。如果你的系统需要"一个即使资源搬了家也稳定的句柄",URN 式的标识符才对;如果需要"一个能点的链接",你要的是 URL。

RFC 3986 的通用语法

一个 URI 被拆成五个部分:

scheme://authority/path?query#fragment
  • scheme —— http、https、mailto…,到第一个 : 为止。
  • authority —— // 之后的部分:user@host:port。
  • path —— authority 之后那层层级路径。
  • query —— ? 之后的部分,也就是常见的 key=value。
  • fragment —— # 之后,只由客户端处理,永远不会发给服务器。

弄清楚这几段很重要,因为不同段落的编码规则不一样:路径里的 / 是分隔符,但查询值里的 / 必须写成 %2F。它也重要,因为有些段会发给服务器,有些不会。

服务器永远看不到的那段:fragment

片段(# 之后的所有内容)只由浏览器处理。它用于页内锚点,也用于那些在客户端路由的单页应用。如果你把敏感数据放在 # 后面、以为"反正它在 URL 里所以安全",请记住片段永远不会进入 HTTP 请求——但它会出现在 document.location 里,会出现在任何复制完整 URL 的分析或日志里,并且在部分配置下还会通过 Referer 头发给其它源。把它当成公开信息,别当秘密。

在代码里解析 URI

别用 string.split('/') 之类的手法去切 URL。每种语言都自带真正的解析器:

// JavaScript:URL 对象
const u = new URL('https://[email protected]:8080/a/b?x=1#frag');
u.protocol;  // "https:"
u.host;      // "ex.com:8080"
u.hostname;  // "ex.com"
u.port;      // "8080"
u.pathname;  // "/a/b"
u.search;    // "?x=1"
u.hash;      // "#frag"

# Python
from urllib.parse import urlsplit
p = urlsplit('https://[email protected]:8080/a/b?x=1#frag')
p.scheme, p.netloc, p.path, p.query, p.fragment

解析器能正确处理你手写会出错的边界情况:userinfo、方括号里的 IPv6 字面量,以及只在恰当位置做百分号解码。

相对引用与 base URI 解析

像 ../img/logo.png 这种叫相对引用。要把它还原成绝对 URI,得拿一个基准 URI(通常是包含它的那个文档的地址)去解析。规则是机械的:砍掉当前路径最后一段,应用 ..,再拼上。基准解析错了,是网站换域名后资源大面积 404 的常见原因。

new URL('../img/logo.png', 'https://ex.com/blog/post/').href;
// "https://ex.com/blog/../img/logo.png" 解析后 -> "https://ex.com/img/logo.png"

data: 以及其它非定位符的 URI

不是每个 URI 都是网络地址。一个 data: URI 把资源内联嵌入,例如 data:text/plain;base64,SGVsbG8=。它是合法的 URI,也是合法的 URL(它有协议,也告诉你怎么获取资源——解码载荷即可)。但它也是不受信输入构建时的 XSS 最爱载体,所以绝不要把用户文本不加严格白名单地塞进 data: URI。

为什么我们嘴上说 URL、规范里写 URI

现在的 WHATWG 和 RFC 文档其实也用"URL"了,但更早的 RFC(以及很多教材)用"URI",因为这个词本来就想把定位符、名称符和一切标识都涵盖进去。实际开发中,你说"URL"指的就是地址栏那个网址,这种用语漂移除了在规范里需要精确,其它场合无伤大雅。

这个区分什么时候真的重要

  • 签名算法。如果你对规范化后的请求串做签名,必须决定签的是 URI 形式还是 URL 形式。签错了规范化方式,攻击者就能改路径却仍然拿着合法签名。
  • 重定向校验。登录后经常要把用户重定向到他提供的 Location。如果你只校验了协议、却忘了 //evil.com 是协议相对 URL,就会留下一个开放重定向漏洞。
  • SSRF 防护。校验用户给的 URI 意味着要完整解析它——协议、主机、端口——而不是做字符串匹配。只做子串检查,会漏掉 https://[email protected]/ 这类把戏。
  • 日志与存储。如果你存的是"URI"、后来又把它当"能 fetch 的 URL"来用,你可能去 fetch 一个 URN 而失败,或者 fetch 了一个你没预料到的 data: URI。

一个重定向 bug,复现一下

协议相对的坑值得具体看一眼。一个只屏蔽 http:// 和 https:// 前缀的朴素校验器,仍然会放 //evil.com 过去,因为它是一个相对 URL、会拿当前协议去解析:

// 朴素检查
function isSafe(target) {
  return !/^https?:\/\//i.test(target);  // 屏蔽了 http(s):// 但……
}
isSafe('//evil.com/x');   // 返回 true —— 错了,它仍然是个绝对化的 URL

// 更好:解析出来,明确要求 http/https 协议
function isSafe2(target) {
  try { return new URL(target).protocol === 'http:' || new URL(target).protocol === 'https:'; }
  catch { return false; }
}

什么时候只是学究

如果你是在写博客链接、或者告诉同事点哪,用"URL"完全正确,换成"URI"反而做作。只有当你讨论的"定位符 vs 名称符 vs 标识符"这个差别会影响结果时,才需要搬出"URI"。

协议名的大小写与规范化

按规范,协议名是大小写不敏感的,所以 HTTPS:// 和 https:// 是同一个 URL。大多数解析器会帮你转成小写,而你做的任何比较或签名都应该先规范化协议名。主机名同理:EX.COM 和 ex.com 是同一个 authority。如果你的安全校验拿原始字符串直接比、没做小写化,攻击者就能用 Ex.com 溜过去。任何比较之前,先规范协议名和主机名。

国际化域名与 punycode

当主机名包含非 ASCII 字符时,浏览器在发请求前会把它转成 punycode(xn-- 开头的形式)。所以你在地址栏看到的 https://例子.com,实际发出去的是 https://xn--fsqu00a.com。地址栏里那个 Unicode 形式只是给你看的。如果你解析一个 URL 去读 hostname,拿到的是 punycode 形式,不是那个漂亮的中文——日志或主机匹配时记住这点,否则你会困惑为什么自己的 "example.com" 检查永远对不上。

为什么这又绕回了编码

五段式语法的每一段都有自己的编码规则,这正是 URL/URI 之分要紧的原因:路径、查询、片段编码方式各不相同,而一个懂"段"的解析器会给每一段正确地做百分号编码。手写 URL 要是无视这些段,就是前面那几篇编码文章里每一个 bug 的根源——所以把语法当合同,别当装饰。

相对引用拼接时的隐藏坑

base URI 解析还有一个容易被忽略的安全点:当你用某个用户给的相对路径去拼绝对 URL 时,如果基准本身带查询或片段,结果会出人意料。比如基准是 https://ex.com/search?q=cat,你相对拼一个 result,得到的是 https://ex.com/result,原来的 ?q=cat 直接没了——因为相对引用替换的是整个路径及之后的部分。要做"在路径后追加一段"这种操作时,先拿 URL 对象解析基准、改 pathname、再读 href,别用字符串拼接,否则查询串和片段都会神秘消失。

小结

URI 是大家族,URL 是你每天用的那个成员,URN 是负责命名的表亲。把五段式语法记牢,因为编码和解析都依赖它;至于 URL/URI 这个措辞,只在安全或签名决策悬于其上时再去较真;并且永远用真正的 URL/URI 解析器,而不是手写按斜杠切分。

继续阅读