使用某物和理解某物之间的差距
在我职业生涯的早期,我在调试一个认证问题,一个高级开发者问道:「JWT过期了吗?」
我说是的,我查过了。我没有承认的是:我把令牌粘贴到一个网站上,看到了过期时间戳,但仍然不完全理解我在看什么。它安全吗?有人能读取这个令牌吗?它被加密了吗?如果它据说是「签名」的和「安全」的,为什么任何网站都能解码它?
这篇指南是为当初那个版本的我写的。三个在Web开发中频繁出现的概念,大多数教程假设你已经了解,实际上一旦解释清楚就很简单。
Base64:不是加密,不是压缩,只是翻译。
最重要的事情先说:Base64不是安全机制。它不隐藏你的数据。任何人都可以立即解码它。如果你曾经想过「这看起来是编码的,所以放在URL里可能是安全的」——不是的。
Base64是一种仅使用可打印ASCII字符来表示二进制数据的方式。就这样。它存在的实际原因:二进制数据会破坏东西。电子邮件、HTTP头部、URL、HTML属性——这些系统是为文本设计的。原始二进制数据,带着它的空字节和控制字符,在通过基于文本的系统时可能会被损坏或改变。
Base64将二进制数据转换为64个字符的安全字母表:A-Z、a-z、0-9、+和/。输出比输入大约大33%,但保证能完整地通过任何文本系统传输。
你真正会遇到它的地方
HTTP基本认证:当你用用户名:密码进行认证时,凭据会被Base64编码并在Authorization: Basic头部中发送。这就是为什么明文HTTP上的Basic Auth是危险的——任何拦截请求的人都可以立即解码凭据。HTTPS是必须的。
JWT令牌:JWT令牌的三个部分中的每一个都是用Base64url编码的(一种将+和/替换为-和_的变体,使其对URL安全)。
Data URI:<img src="data:image/png;base64,iVBORw0KGgo...">——将图片数据直接嵌入HTML或CSS,而不是发出单独的HTTP请求。
电子邮件附件:MIME编码使用Base64,这样二进制文件就可以通过只为文本设计的邮件服务器传输。
自己试试:在Base64编码器中粘贴任何文本并查看结果。然后把编码后的字符串粘贴回去解码。注意「编码」版本没有隐藏任何东西——它只是使用了不同的字母表。
JWT:三个Base64字符串和一个签名
一个JSON Web令牌看起来像这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxMjMiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJleHAiOjE3MTM5MzYwMDB9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
恰好两个点。三个部分。
头部——第一个点之前的部分。解码为关于令牌的元数据:使用了哪种签名算法,是什么类型的令牌。
载荷——中间部分。解码为实际的声明:用户ID、电子邮件、角色、过期时间。这是你的后端读取来识别用户的内容。
签名——第二个点之后的部分。令牌未被篡改的密码学证明。
大多数开发者感到惊讶的地方
载荷是Base64url编码的。它没有被加密。任何拥有令牌的人都可以解码载荷并读取其内容——用户ID、电子邮件、角色、过期时间,以及你放在那里的任何内容。
把任何JWT粘贴到JWT解码器中,看看里面究竟有什么。不需要任何密钥。
这让人们感到惊讶,因为字符串看起来像不透明的乱码。它不是。JWT的安全模型是:「我可以验证这个令牌是由我的服务器真正颁发的(签名验证),所以我可以信任它包含的声明。但这些声明本身并不是秘密。」
实际影响:不要把密码、支付数据或敏感个人信息放入JWT载荷。只放识别和授权用户所必需的信息。
签名真正保证了什么
签名是通过将头部+载荷+只有你的服务器知道的密钥传入哈希函数来创建的。当你的服务器收到令牌时,它用同样的密钥重新计算签名。如果重新计算的签名与令牌中的签名匹配,令牌是真实的且未被修改。
如果有人修改载荷将用户ID从123改为456,签名验证就会失败。
这就是为什么你的JWT签名密钥必须真正保密。如果攻击者获得了它,他可以伪造令牌并冒充任何用户。
哈希:单向函数
哈希函数接受任何输入并产生固定大小的输出。两个属性使它对安全性有用:
确定性:相同的输入总是产生相同的输出。
单向性:你不能反转哈希来获得原始输入。不存在unhash()。
SHA-256("密码123") → ef92b778bafe771207...
SHA-256("密码124") → 88d4266fd4e6338d13...
一个字符的差异。完全不同的哈希。相似输入和相似输出之间没有数学关系。
为什么密码被哈希而不是加密
如果密码被加密,你可以用正确的密钥解密它们。如果攻击者窃取了你的数据库和加密密钥,每个用户的密码都会暴露。
使用哈希:你存储哈希,而不是密码。当用户登录时,你对他们输入的内容进行哈希,并与存储的哈希进行比较。你永远不需要恢复原始内容——只需验证一次尝试。
即使攻击者获得了你的数据库,他们也只有哈希值。要获得密码,他们必须猜测并验证,这要慢得多。
MD5、SHA-256、bcrypt——为什么使用哪个很重要
MD5在2000年代初是标准。现在它在安全目的上已经被破解。现代GPU每秒可以计算数十亿个MD5哈希,这意味着拥有你数据库的攻击者可以在几分钟内破解简单密码。永远不要将MD5用于密码。
SHA-256适用于完整性验证——确认文件没有被损坏,为webhook生成HMAC签名。但它对密码来说太快了。快速哈希对密码不好,因为它使暴力攻击变得廉价。
bcrypt专门为密码设计。它故意很慢——你可以调整它执行多少轮计算——并包含内置的「盐」(随机数据),确保两个相同的密码产生完全不同的哈希。
规则:对密码使用bcrypt(或Argon2或scrypt)。对其他所有需要完整性验证的内容使用SHA-256。
快速参考
| 概念 | 是什么 | 可逆吗? | 用途 |
|---|---|---|---|
| Base64 | 二进制→文本编码 | 是,轻而易举 | 通过文本系统传输二进制数据 |
| JWT | 签名的JSON令牌 | 载荷:是。签名:否 | 无状态认证 |
| SHA-256 | 哈希函数 | 否 | 文件完整性,HMAC签名 |
| bcrypt | 密码哈希 | 否 | 存储用户密码 |
正确理解这三件事可以防止一类安全错误——这类错误在代码审查中看起来是正确的,只在生产中才被发现——那时损害已经造成。