身份验证就像是你应用程序门口的保安。每当有人注册或登录时,你的系统都必须决定他们是否真的是其声称的身份。如果处理不当,你面临的不仅仅是调试一次失败的登录,你还在冒着让真实用户数据直接流失的风险。要妥善实现这一点,需要两种可靠的工具:用于保护静态密码的 Bcrypt,以及用于在用户浏览应用时验证身份的 JSON Web Tokens。

为什么明文存储是不可行的

如果你只能从本文中记住一条规则,请记住这一条:永远不要在数据库中以明文形式存储密码。无论你的数据库是否位于防火墙之后,或者你是否信任团队中的每一位工程师,这都无关紧要。以原始形式将密码写入表中,就像把家门钥匙放在欢迎垫下面一样。一旦有人通过配置错误的 API、泄露的备份或注入攻击获得了该数据库的访问权限,所有的凭据都会瞬间暴露。

由于人们会重复使用密码,这种破坏力会成倍增加。一次单一的泄露不仅会危及你的应用程序,还会危及用户的电子邮件、银行和社交媒体账户。这就是我们为什么要对密码进行哈希处理的原因。哈希处理将密码转换为一段杂乱无章的字符串,与原始密码没有任何明显的相似之处。这个过程是单向且不可逆的。你无法向哈希值倾倒“数学酸液”并将其溶解回创建它的原始密码。

使用 Bcrypt 进行密码哈希处理

Bcrypt 是专门为密码设计的哈希函数。它接收明文字符串,通过 Blowfish 加密算法进行处理,然后返回一个类似于 $2b$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy 的结果。前缀会告诉你算法和成本因子(cost factor)。长尾部分则是盐值(salt)和哈希值本身的组合。

盐值是在开始哈希处理之前混入密码中的随机数据。由于每个用户的盐值都是唯一的,即使两个人都选择 "password123",他们在数据库中最终得到的哈希值也会完全不同。这种微小的差异可以破解彩虹表(rainbow tables)——即针对常用密码预先计算好的哈希字典——因为攻击者需要为每一个唯一的盐值重新生成整个表格。

Bcrypt 也是刻意设计得很慢。使用像 SHA-256 这样快速的算法,现代硬件每秒可以猜测数十亿个哈希值。而 Bcrypt 则会“拖慢脚步”。它会多次运行哈希过程,并由一个成本因子控制,随着计算机性能的提升,你可以提高这个因子。这种缓慢会惩罚任何试图通过暴力破解手段攻破被盗数据库的人。对于在登录时多等待两百毫秒的正当用户来说,几乎察觉不到。但对于试图测试数百万次猜测的攻击者来说,这绝对是致命的。

JSON Web Tokens 的工作原理

如果说 Bcrypt 处理的是大门,那么 JWT 处理的就是“走廊通行证”。JSON Web Token 是一种紧凑且 URL 安全的字符串,用于证明用户已经通过了身份验证。可以把它想象成由服务器签发、客户端随身携带的数字身份证。

一个 JWT 包含三个由点分隔的部分:头部(header)、负载(payload)和签名(signature)。头部指定了令牌类型和签名算法。负载携带了声明(claims)——即关于用户和令牌本身的信息——例如用户 ID、用户名和过期时间戳。签名则是加密印章。服务器通过对头部和负载进行编码,然后使用密钥进行处理来创建签名。如果有人篡改了负载,签名将不再匹配,服务器会直接拒绝该令牌。