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

发布: (2026年2月7日 GMT+8 15:10)
5 分钟阅读
原文: Dev.to

Source: Dev.to

《停止像1999年那样存储密码》封面图:Node.js + MySQL 现实检验

你正在制造的恐怖

你刚刚构建了第一个 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 注入。
  • 永不以明文形式存储密码。

你的用户信任你——别成为明天的泄露头条。在互联网发现之前,先确保密码安全。

0 浏览
Back to Blog

相关文章

阅读更多 »

UX/UI 排版

Typography 是指什么?- 使用哪种字体 - 在什么位置多大 - 多粗 - 行间距 - …