跳到主要内容
LeedyBox
支持我们

Base64、URL 编码与 JWT 详解

只要数据需要以文本形式传输,比如在 URL、HTTP 头、JSON 和登录令牌中,就会遇到这三者。但它们都不会隐藏任何内容。本指南说明它们各自的工作原理、出现的场合,以及导致大多数问题的错误。

编码不是加密

编码是改变数据的书写方式,让它能通过只接受特定字符的通道。由于不涉及密钥,任何人都能把它还原。加密也会改变数据,但只有持有密钥的人才能还原。哈希又不一样:它是一种无法还原的单向指纹,详见哈希函数详解。

Base64 和 URL 编码都属于编码。JWT 通常带有签名,可以证明是谁签发的、内容没有被改动,但其内容本身只是经过了编码。如果某个值必须保密,需要的是加密,而不是这些格式。

Base64 的工作原理

Base64 每次读取 3 个字节(24 位),把它们分成 4 组,每组 6 位。每组是 0 到 63 之间的一个数,用 64 个字符组成的字母表中的一个字符来表示:0–25 是 A–Z,26–51 是 a–z,52–61 是 0–9,然后是 + 和 /。用单词“Man”来看看各个步骤:

步骤Man
字节值7797110
二进制位010011010110000101101110
4 组 6 位010011 · 010110 · 000101 · 101110 = 19 · 22 · 5 · 46
Base64T W F u → TWFu

当输入长度不是 3 的倍数时,最后一组会被补齐并用填充标记:剩下 1 个字节时得到 2 个字符和 ==,剩下 2 个字节时得到 3 个字符和 =。“Ma”变成 TWE=,“M”变成 TQ==。

由于每 3 个字节变成 4 个字符,Base64 会让数据增大约三分之一,某些格式还会再加上换行。作为邮件附件发送的 3 MB 照片,传输时大约是 4 MB 的文本;以数据 URL 嵌入样式表的图片,也比原文件大约重三分之一。

在哪里会遇到 Base64

  • 邮件附件(MIME)是每行 76 个字符换行的 Base64 文本。
  • 数据 URL,例如 data:image/png;base64,iVBORw0…,把小文件直接放进 HTML 或 CSS。
  • HTTP 基本认证把 user:password 转成 Base64 放在 Authorization 头中发送。这只能让密码不被一眼看出,因此只有在 HTTPS 下才安全。
  • 证书和密钥的 PEM 文件,是位于 -----BEGIN …----- 和 -----END …----- 两行之间、每行 64 个字符的 Base64。
  • JSON 和 XML 没有二进制类型,因此用 Base64 字符串承载签名、缩略图等二进制值。

Base64URL 与填充

标准 Base64 中的 + 和 / 在 URL 和文件路径里有特殊含义,因此第二种字母表 Base64URL 改用 - 和 _。由于解码器能算出长度,它通常也省略 = 填充。JWT、许多 API 令牌和一些文件名都使用 Base64URL。要把标准 Base64 转成 Base64URL,把 + 换成 -、/ 换成 _,并删去末尾的 =;反过来时,撤销替换,再补上 = 直到长度是 4 的倍数。

URL 中的百分号编码

URL 只能包含有限的字符,其中一些还用来表示地址的结构:/ 和 ? 分隔路径和查询,& 和 = 分隔参数,# 表示片段的开始。当一个值包含这些字符或任何非 ASCII 字符时,就要进行百分号编码:把它的 UTF-8 形式的每个字节写成 % 加两位十六进制数。é 是 C3 A9 两个字节,所以“café”会变成 caf%C3%A9。

字母、数字以及 - . _ ~ 永远不需要编码。最常引起问题的字符如下:

字符编码后为什么重要
空格%20URL 中不允许出现
&%26会开始一个新参数
=%3D会结束参数名
?%3F会开始查询部分
#%23会开始片段,而片段不会发送到服务器
/%2F会增加一级路径
+%2B在表单数据中会被当作空格
%%25会开始一个转义序列

先逐个编码各个值,再拼装 URL。在 JavaScript 中,encodeURIComponent 用于编码单个值,也会编码 / ? & = #;而 encodeURI 用于整个地址,会保留这些字符。对单个值使用 encodeURI 是查询字符串出错的常见原因。

加号、空格与重复编码

HTML 表单以 application/x-www-form-urlencoded 格式发送字段,其中空格会变成 +。而在一般的 URL 语法中,+ 只是一个加号。因此,把“C++”的搜索以 q=C++ 发送时,在解码表单数据的服务器上会变成“C”后面跟两个空格;正确的写法是 q=C%2B%2B。手动解码查询字符串时,先弄清楚它是按哪种规则生成的。

重复编码是指把已经编码过的值再编码一次:% 本身会变成 %25,所以 %20 会变成 %2520。如果在 URL 中看到 %25 后面跟着两位十六进制数,说明链路中某处编码了两次;先解码一次,再查找多出来的那一步在哪里。

JWT 里有什么

JSON Web Token(JWT)把各种声明(例如用户是谁、令牌在什么时间前有效)从签发它的系统传给接受它的系统。常见的已签名形式由点分隔的三段 Base64URL 组成:头部、载荷和签名。前两段解码后就是普通的 JSON:

Header:  {"alg": "HS256", "typ": "JWT"}
Payload: {"sub": "user-42", "role": "editor", "iat": 1767225600, "exp": 1767229200}

标准(RFC 7519)定义了几个注册声明;应用程序还会添加自己的声明,例如上面的 role。

声明含义
iss签发者:创建令牌的一方
sub主体:通常是用户或账户 ID
aud受众:令牌面向的服务
exp, nbf, iat过期时间、生效时间、签发时间:以秒为单位的 Unix 时间
jti令牌 ID:用于发现重复使用或吊销单个令牌

由于载荷只是经过编码,持有令牌的任何人都能读到它。不要在其中放入个人数据和机密信息,并让令牌保持小巧:每个请求都会带上它,而许多服务器会限制请求头的大小。

有签名不等于保密:如何校验 JWT

签名是根据头部和载荷计算出来的。使用 HS256 时,签发方和接收方共享同一个密钥,采用基于 SHA-256 的 HMAC。使用 RS256 或 ES256 时,签发方用私钥签名,接收方用对应的公钥校验,因此接收方无法自行创建令牌。

接收令牌的服务器在信任任何声明之前,必须用预期的算法和密钥校验签名,并检查 exp、nbf、iss 和 aud。像 LeedyBox 工具那样只做解码,会跳过所有这些步骤:它显示令牌写了什么,而不是令牌是否真实。库还必须拒绝表示未签名令牌的“none”算法,除非本来就预期使用未签名令牌。如果内容本身必须隐藏,就要对令牌加密(JWE),这时令牌由五段组成,没有密钥就无法读取。

常见问题与解决办法

现象可能原因解决办法
Base64 解码后是乱码数据是二进制,或是其他编码的文本确认编码的是什么;二进制数据请另存为文件
解码时提示“无效字符”带有 data: 前缀、引号,或使用了 Base64URL 字母表删去逗号之前的所有内容;使用接受 - 和 _ 的解码器
参数被截断值中有未编码的 & 或 #用 encodeURIComponent 逐个编码各个值
URL 中出现 %2520值被编码了两次只在构建 URL 的地方编码一次
exp 还没到,令牌却被拒绝受众或签发者不对、时钟偏差或密钥不匹配把 aud、iss 和签名密钥与服务器的期望值逐一比对
过期日期在几千年后exp 写成了毫秒而不是秒以秒为单位存储 exp、nbf 和 iat

常见问题

Base64 能用来保护密码或 API 密钥吗?

不能。任何人都能在一秒钟内还原 Base64。放在配置文件或 URL 中的 Base64 编码凭据,和明文一样暴露。

可以把个人数据放进 JWT 载荷吗?

尽量避免。拿到令牌的任何一方都能读到载荷,包括浏览器扩展、日志和能看到请求的代理。令牌中只放一个 ID,详细信息在服务器端查询。

URL 编码能防止注入攻击吗?

只是在“值无法破坏 URL 结构”这一意义上有效。服务器仍须针对解码后的值的实际用途(如 SQL、HTML 或 shell 命令)进行校验和转义。

为什么 Base64 字符串比原文件还长?

每 3 个字节变成 4 个字符,所以文本会大约增大 33%,换行还会再增加一些。对于大文件,直接发送字节比使用 Base64 更高效。