别再像1999年那样存储密码:Node.js + MySQL 现实检验
Source: Dev.to

你正在制造的恐怖
你刚刚构建了第一个 Node.js 认证系统。感觉自己很棒。然后你写了下面这段代码:
app.post('/register', (req, res) => {
const { username, password } = req.body;
db.query('INSERT INTO users (username, password) VALUES (?, ?)',
[username, password]); // 🚨 DISASTER
});
恭喜!当你的数据库泄漏(何时泄漏,迟早会泄漏)时,所有密码就像 Costco 的免费样品一样裸露。人们在各处重复使用密码。那个 "fluffy2023"?你刚刚把黑客通向 Sarah 的电子邮件、银行账户和 Instagram 的大门打开了。
解决方案:BCrypt 保存一切
BCrypt 将密码混合到几乎无法辨认的程度,且没有“撤销”按钮。黑客看到的只是类似 $2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW 的乱码。
const bcrypt = require('bcrypt');
app.post('/register', async (req, res) => {
const { username, password } = req.body;
const hashedPassword = await bcrypt.hash(password, 10);
db.query('INSERT INTO users (username, password) VALUES (?, ?)',
[username, hashedPassword]);
});
10 是 盐轮数——表示加密循环的次数。对于 2025 年来说,十次是一个不错的默认值。
错误 #2:比较方式错误
你已经对密码做了哈希处理?别用下面这种方式毁了它:
// WRONG - Never works
if (results[0].password === password) {
res.send('Logged in!');
}
不能使用 === 将哈希值与明文进行比较。解决办法:
app.post('/login', async (req, res) => {
const { username, password } = req.body;
db.query('SELECT * FROM users WHERE username = ?', [username],
async (err, results) => {
const match = await bcrypt.compare(password, results[0].password);
if (match) {
res.send('Welcome!');
} else {
res.send('Try again');
}
});
});
bcrypt.compare() 能安全地处理验证。
错误 #3:缺乏验证
接受 任何 密码等同于自杀。 "1"? 空字符串? 当然!
function isPasswordValid(password) {
const regex = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/;
return regex.test(password);
}
app.post('/register', async (req, res) => {
if (!isPasswordValid(req.body.password)) {
return res.status(400).send('Need 8+ chars, letters, numbers & symbols');
}
// continue with hashing...
});
错误 #4:SQL 注入
使用模板字符串构建查询会招致灾难:
// NEVER
const query = `SELECT * FROM users WHERE username = '${username}'`;
像 admin'-- 这样的输入可以绕过身份验证。始终使用参数化查询:
db.query('SELECT * FROM users WHERE username = ?', [username]);
? 占位符会将输入视为数据,而不是代码。
完整安全示例
const express = require('express');
const bcrypt = require('bcrypt');
const mysql = require('mysql2/promise');
const app = express();
app.use(express.json());
async function initDb() {
return mysql.createPool({
host: 'localhost',
user: 'root',
password: 'your_password',
database: 'your_db',
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
});
}
const db = await initDb();
function isPasswordValid(password) {
const regex = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/;
return regex.test(password);
}
app.post('/register', async (req, res) => {
const { username, password } = req.body;
if (!isPasswordValid(password)) {
return res.status(400).json({ error: 'Weak password!' });
}
const hashedPassword = await bcrypt.hash(password, 10);
await db.query('INSERT INTO users (username, password) VALUES (?, ?)',
[username, hashedPassword]);
res.json({ message: 'Account created!' });
});
app.post('/login', async (req, res) => {
const { username, password } = req.body;
const [results] = await db.query('SELECT * FROM users WHERE username = ?',
[username]);
if (!results.length) {
return res.status(401).send('Invalid credentials');
}
const match = await bcrypt.compare(password, results[0].password);
if (match) {
res.json({ message: 'Login successful!' });
} else {
res.status(401).send('Invalid credentials');
}
});
app.listen(3000, () => console.log('Server running on port 3000'));
警钟
现在是 2025 年。AI 编写代码,汽车自动驾驶。没有任何借口可以像 MySpace 时代那样存储密码。
- 使用 BCrypt(或 Argon2)对密码进行哈希。
- 验证输入强度。
- 使用参数化查询防止 SQL 注入。
- 永不以明文形式存储密码。
你的用户信任你——别成为明天的泄露头条。在互联网发现之前,先确保密码安全。