开发者编码手册:URL、Base64 与 JWT 详解

URL 编码、Base64 和 JWT,是日常开发里最容易用错的三样东西。它们看着都懂,一用就容易错。它们分别解决传输、表示和身份声明问题,但都只是编码,不是加密。这篇手册把规则、原理和调试技巧一次讲清楚。

为什么需要编码

网络里传数据,前提是双方对“哪里是分隔符、哪里是内容”有共识。URL 用 ?、&、= 划分参数。如果参数值本身包含这些字符,就会被误解成结构。邮件、HTTP 头这类早期协议只保证 7 位 ASCII 可靠传输,中文、图片这类字节数据需要一种百搭的文本表示法。编码做的事情是用双方约定的规则改写数据形态,保证它在特定通道里不被误读。它不提供任何保密性。

URL 编码:%XX 的规则

URL 编码(百分号编码)把不安全字符替换成 % 加两位十六进制字节值。空格变成 %20。“a=1&b=2”作为参数值,必须写成 a%3D1%26b%3D2,否则 & 会被当成参数分隔符。字母、数字和 - _ . ~ 这四个字符属于“非保留字符”,原样保留。

JavaScript 里两个内置函数的区别,是高频面试题,也是高频事故源。

  • encodeURIComponent:编码除 A–Z、a–z、0–9 和 - _ . ! ~ * ' ( ) 之外的所有字符,适合编码参数值。
  • encodeURI:额外保留 : / ? # 等 URL 结构字符,适合编码整条 URL。

典型错误是用 encodeURI 处理查询参数。结果 & 和 = 原样通过,服务端解析出一堆错误参数。另外注意:加号 + 只在表单提交(application/x-www-form-urlencoded)里表示空格。在路径里,它就是加号本身。拿不准时,把样本贴进本站的 URL 编解码工具对照一遍即可。

Base64:3 字节怎么变成 4 个字符

Base64 把每 3 个字节(24 位)拆成 4 组各 6 位。每组在 64 个字符的字母表(A–Z、a–z、0–9、+、/)里对应一个符号。以 “Man” 为例,三个字节拆成 010011、010110、000101、101110,对应 “TWFu”。字节数不是 3 的倍数时用 = 补位:剩 1 字节补两个 =,剩 2 字节补一个 =。因此 Base64 文本的体积恒定膨胀约三分之一。

URL 场景里 + 和 / 会与保留字符冲突,于是有了 URL-safe 变体:用 - 和 _ 替换,并通常省略补位符。JWT 用的就是它。

Base64 不是加密

Base64 没有密钥,任何人拿到文本都能一步还原。它的作用只是把任意字节安全地塞进纯文本通道。把密码“Base64 一下”当加密,属于经典事故:数据库泄露时,攻击者会顺手解码。真正需要保密,请用 AES 这类对称加密。需要校验内容是否被篡改,就用本站的哈希生成器计算 SHA-256 等摘要值进行比对。

JWT:三段式的身份凭证

JWT 由两个点号分成三段:header.payload.signature。前两段都是 Base64URL 编码的 JSON。header 声明签名算法(如 HS256),payload 携带 claims。常见 claims 有:iss(签发者)、sub(主体,通常是用户 ID)、aud(受众)。还有 exp(过期时间戳)、iat(签发时间)、jti(令牌唯一编号)。第三段签名按 header 指定的算法,对前两段计算而来。

HS256 与 RS256 的区别在密钥结构。HS256 是对称算法,签发和校验共用同一个密钥,适合单体应用。RS256 是非对称算法,私钥签发、公钥校验,校验方拿不到签发能力,适合多服务、第三方接入的场景。

过期、撤销与调试技巧

JWT 无状态,签出去就收不回来:服务端不查库,就不知道某个令牌是否已“作废”。通行做法是把 exp 设得很短,几分钟到几十分钟,再配合刷新令牌续期。需要紧急封禁时,把 jti 或用户版本号加入黑名单,代价是重新引入了状态。切记 payload 只是编码,任何拿到令牌的人都能直接读,绝不能放敏感数据。

调试签名错误有个笨但有效的办法:把令牌按点号拆成三段,逐段用本站的 JWT 解析工具或 Base64 编解码工具解码。确认 header 里的 alg 与服务端配置一致,payload 里的 exp 是否已过期,签名段是否完整(有些网关会截断)。大多数 “signature verification failed” 并不是算法被破解。常见原因是密钥不对、令牌被截断,或校验时重新编码了 payload,与签名时的字节不一致。

常见错误与排查

编码类 bug 的共同点是“看起来都对,字节不对”,排查思路也一致:把中间结果打印出来逐字节比对。下面几条在生产环境反复出现。

  • 对已经编码过的值再编码一次。错在:模板里的 %20 传到前端又被编码成 %2520,服务端解码一次还剩下 %2520。中文则变成层层嵌套的 %25 序列。双重编码是联调时最高频的“两边都没错但结果不对”,因为它藏在链路的某一环里。正确做法:约定编码只发生一次,在数据出口处做。排查时在日志里搜 %25,出现即说明被编码了两次,把多余那次删掉。
  • 用 btoa 直接编码中文。错在:btoa 只接受 Latin-1 范围内的字符,遇到“中”字直接抛 InvalidCharacterError。用 unescape(encodeURIComponent(s)) 这种老写法补救,遇到 emoji 等增补平面字符仍会出错。正确做法:先用 TextEncoder 把字符串转成 UTF-8 字节,再对字节做 Base64。解码时按相反顺序还原。
  • 把 JWT 拼在 URL 查询参数里传递。错在:令牌会原样落进网关与服务器的访问日志、浏览器历史,还可能通过 Referer 头发给第三方资源。日志一存数年,等于把长期有效的身份凭证写进了最容易泄露的地方。正确做法:放进 Authorization 请求头。确需一次性链接(如邮箱验证),用短 exp 的一次性令牌,并在使用后立即作废。
  • 用标准 Base64 去解 JWT 的三段。错在:JWT 用的是 Base64URL 变体,字母表里的 - 和 _ 不在标准 Base64 的字符表里,很多解码器直接报错或静默失败。补位符 = 又常被省略,硬按 4 字符分组会错位。正确做法:解码前把 - 换成 +、_ 换成 /,按需补齐 =。或直接用支持 Base64URL 的解析工具。
  • 校验签名时信任 header 里的 alg 字段。错在:攻击者可以把 alg 改成 none 绕过验签。还有一种 RS256/HS256 混淆攻击,拿公开的 RSA 公钥当 HS256 的密钥去伪造签名。服务端照 header 选算法,等于让攻击者挑锁。正确做法:服务端把算法写死成白名单,只接受预期的那一个。拒绝 alg 为 none 或不在白名单内的令牌。
  • 验签前把 payload 重新 JSON.stringify 一遍。错在:签名是对编码后的原始字节算的。重新序列化会改变键序或空白,字节不一致,签名必然失败。报错却指向“密钥错误”,把排查带进死胡同。正确做法:验签用收到的原始两段字符串。解析出的对象只用来读取 claims。

文中相关工具