为什么你的 <b> 变成了 %3Cb%3E?一文读懂 URL 编码机制与误区
|
zhenglin
2026年2月3日 16:54
本文热度 3867
|
摘要:在前端开发与安全测试中,我们常看到 URL 参数被自动转换成 % 开头的乱码。很多人误以为这是某种安全加密或 XSS 防御机制,但事实并非如此。
本文将从 RFC 标准出发,解析 URL 编码的底层逻辑、触发时机以及它与 Web 安全的真实关系。
1. 现象还原
在进行 Web 安全测试(如 XSS 注入)时,我们尝试在 URL 参数中输入 HTML 标签:
http://localhost:8080/api?name=<b>test</b>
然而,当我们查看网络请求(Network)或地址栏时,发现它变成了:
http://localhost:8080/api?name=%3Cb%3Etest%3C%2Fb%3E
初学者容易产生两个误区:
误区一:这是浏览器在帮我拦截 XSS 攻击。
误区二:后端收到的就是乱码,需要手动写代码解码。
真相是:这只是 HTTP 协议为了“交通合规”做的标准动作,与安全防御毫无关系。
2. 为什么要编码?(RFC 3986 标准)
URL(统一资源定位符)的设计初衷是只能通过 ASCII 字符集在互联网上传输。为了避免歧义和保证传输安全,IETF 在 RFC 3986 标准中将字符分为了三类:
2.1 绝对安全的字符(Unreserved)
不需要编码,走到哪里都是原样。
2.2 保留字符(Reserved)
在 URL 结构中有特殊语法含义的字符。
2.3 不安全字符(Unsafe)
容易引起解析歧义或不可打印的字符。
3. 编码原理:百分号编码 (Percent-Encoding)
URL 编码的核心算法非常简单:Hex(UTF-8) + % 。
字符集确定:现代 Web 浏览器默认使用 UTF-8 字符集。
转字节:将字符转换为其对应的 16 进制字节序列。
加前缀:在每个字节前加上 %。
以字符 < 为例:
ASCII 码值:60
16 进制:3C
编码结果:%3C
以中文 “测” 为例:
UTF-8 字节流:E6 B5 8B
编码结果:%E6%B5%8B
4. 编码与解码的生命周期
理解了这个生命周期,你就会明白为什么 URL 编码防不住 XSS。

关键结论:
后端框架(Spring Boot, Express, Django 等)在将请求交给你的业务代码(Controller)之前,通常已经自动完成了 URL 解码。
如果你在数据库里存入了这个值,存进去的是 <b>,而不是 %3Cb%3E。当这个数据再次被取出来并渲染到页面上时,如果没做HTML 实体转义,浏览器就会执行它。
5. 前端开发实战指南
在 JS 中手动处理 URL 编码时,请务必区分这两个 API: encodeURI vs encodeURIComponent
const param = "A&B"; // 参数值里带了 &
// ❌ 错误做法
const url1 = "http://api.com?q=" + encodeURI(param);
// 结果: ...?q=A&B -> 后端解析为 q="A", 剩下的 B 成了无效部分
// ✅ 正确做法
const url2 = "http://api.com?q=" + encodeURIComponent(param);
// 结果: ...?q=A%26B -> 后端正确解析为 q="A&B"
6. 总结
URL 编码是通信协议层面的规范,目的是让数据在网络传输中“合法”且“无歧义”。
URL 编码不是安全防御。它在到达后端业务逻辑前就会被还原。
防御 XSS 的真正手段是 HTML 实体编码 (HTML Entity Encoding) (如将 < 转义为 <),这通常由前端框架(Vue/React)在渲染层自动完成,或由 WAF 在网关层拦截。
不要被浏览器地址栏里的 % 迷惑,安全防御必须建立在对数据流转的深刻理解之上。
参考文章:原文链接
该文章在 2026/2/3 16:54:38 编辑过