Menu
CoddyTech

切勿存储密码

本课属于 Coddy Node.js 学习旅程的“身份验证与安全”单元。第 4 课,共 24 课。

人们会重复使用密码,因此泄露的明文密码可能会危及其他地方的账户。存储加盐密码哈希:这是一种派生值,你可以通过重新计算来验证它,而不是对其解码。

const salt = randomBytes(16).toString('hex');
const key = await scryptAsync(inviteCode, salt, 32);
const saved = salt + '.' + key.toString('hex');

这个示例将盐和派生密钥组合起来,用于邀请码。分隔符可以让验证过程提取盐,并使用相同的参数派生另一个密钥。你的密码任务使用自己的分隔符和密钥长度。每次创建新密码时都生成盐,派生密钥,然后将两者都存储起来。加盐哈希仍然需要强密码,因为攻击者可以尝试猜测。

challenge icon

挑战

简单

现在,注册时会原样保存用户输入的密码。请改为保存加盐哈希,这样即使数据库泄露,密码也不会被泄露。

  1. 在 passwords.js 中,修改 hashPassword(password),让它返回注册时保存的文本,格式为盐、冒号、哈希:
    • 盐是 16 个随机字节,每次调用时都重新生成,并以十六进制形式写入:由 0 到 9 和 a 到 f 组成的 32 个字符。
    • 哈希是 scrypt 根据密码和盐生成的 64 字节密钥,也以十六进制形式写入:共 128 个字符。scryptAsync 的第三个参数是密钥长度,单位为字节。
    • 用冒号连接这两部分。保存的值看起来像这样:f2bba0669d9fa2d203afc7e61bf8f065:c3f7cae352e023a8d4766da9a316b5a2c844100efff22c4079f89a8ab8512ac91c33227481c3afa88c3ba0e88347f9e3e0c22dad9d55f19688a350f71be8dbd2
  2. 你不需要修改 server.js。它的 POST /signup 已经会调用 hashPassword,并保存该函数返回的内容。它还有 GET /debug/users/:username,可以显示某个用户保存的内容。这个路由只是为了让你查看,真正的应用不会有它。

试试看:按“运行”。发送 POST /signup,请求体为 {"username": "ada", "password": "correct horse"}。你会得到状态码 201 和 {"username": "ada"}。使用同一个请求体,再发送一次,但把用户名改为 "username": "bob"。现在先发送 GET /debug/users/ada,再发送 GET /debug/users/bob。每个 passwordHash 都由 32 个字符、一个冒号和另外 128 个字符组成。即使 ada 和 bob 选择了相同的密码,这两个值也不相同。

亲手试试

import { randomBytes, scrypt } from 'node:crypto';
import { promisify } from 'node:util';

const scryptAsync = promisify(scrypt);

// 把密码转换成 signup 存储的文本。
// 目前会原样保留输入的密码。
export async function hashPassword(password) {
    return password;
}
quiz icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

身份验证与安全中的所有课

自己练习: Express 在线编辑器