Node.js + Mysql 防止sql注入的写法
Node.js + Mysql 防止sql注入的写法
一、背景与问题
在Web开发中,SQL注入是最常见的安全漏洞之一。攻击者通过构造恶意输入,可以绕过应用程序的业务逻辑,直接操作数据库,造成数据泄露、数据篡改甚至数据库被完全控制。
以Node.js + MySQL的典型场景为例,开发人员常使用mysql或mysql2库进行数据库操作。如果直接拼接用户输入到SQL语句中,就可能引发注入攻击。例如:
// 错误写法:直接拼接用户输入
const sql = `SELECT * FROM users WHERE username = '${username}'`;当用户输入' OR '1'='1时,SQL语句会变成:
SELECT * FROM users WHERE username = '' OR '1'='1'这会导致查询返回所有用户记录,从而实现登录绕过。
二、基本原理
SQL注入的核心在于字符串拼接。防御的核心思想是将用户输入与SQL语句分离,通过参数化查询(Prepared Statements)或ORM查询构建器来确保用户输入仅作为参数传递,而非SQL语句的一部分。
MySQL的参数化查询机制通过以下步骤实现:
- 客户端将SQL语句和参数分开发送
- MySQL服务器对SQL语句进行预处理
- 参数以二进制形式传递,自动进行转义处理
- 最终执行安全的SQL语句
三、环境准备
npm install mysql2需要MySQL数据库,创建测试表:
CREATE DATABASE test_db;
USE test_db;
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50),
password VARCHAR(100)
);
INSERT INTO users (username, password) VALUES
('alice', '123456'),
('bob', '654321');四、核心实现
1. 基础参数化查询(mysql2)
const { Pool } = require('mysql2');
const pool = new Pool({
host: 'localhost',
user: 'root',
password: 'your_password',
database: 'test_db'
});
async function getUser(username) {
const [rows] = await pool.query(
'SELECT * FROM users WHERE username = ?',
[username]
);
return rows;
}关键点:
- 使用
?占位符 - 参数作为数组传递
- 自动处理特殊字符转义
2. 使用Sequelize ORM
const { Sequelize, DataTypes } = require('sequelize');
const sequelize = new Sequelize('test_db', 'root', 'your_password', {
host: 'localhost',
dialect: 'mysql'
});
const User = sequelize.define('User', {
username: DataTypes.STRING,
password: DataTypes.STRING
});
async function getUser(username) {
const user = await User.findOne({
where: { username }
});
return user;
}Sequelize会自动处理参数绑定,即使输入包含特殊字符也能安全执行。
3. 使用参数化查询 + 密码哈希
const bcrypt = require('bcrypt');
async function login(username, password) {
const user = await getUser(username);
if (!user) return null;
const isValid = await bcrypt.compare(password, user.password);
return isValid ? user : null;
}注意:密码哈希应使用bcrypt等库处理,而不是直接存储明文。
五、完整案例:用户登录系统
// app.js
const express = require('express');
const { Pool } = require('mysql2');
const bcrypt = require('bcrypt');
const app = express();
const port = 3000;
// 数据库连接
const pool = new Pool({
host: 'localhost',
user: 'root',
password: 'your_password',
database: 'test_db'
});
// 用户注册
app.post('/register', async (req, res) => {
const { username, password } = req.body;
// 防止SQL注入
const hashedPassword = await bcrypt.hash(password, 10);
try {
await pool.query(
'INSERT INTO users (username, password) VALUES (?, ?)',
[username, hashedPassword]
);
res.status(201).send('User registered');
} catch (err) {
res.status(500).send('Error registering user');
}
});
// 用户登录
app.post('/login', async (req, res) => {
const { username, password } = req.body;
try {
const [rows] = await pool.query(
'SELECT * FROM users WHERE username = ?',
[username]
);
if (rows.length === 0) {
return res.status(401).send('User not found');
}
const user = rows[0];
const isValid = await bcrypt.compare(password, user.password);
if (isValid) {
res.send('Login successful');
} else {
res.status(401).send('Invalid password');
}
} catch (err) {
res.status(500).send('Error logging in');
}
});
app.listen(port, () => {
console.log(`App running at http://localhost:${port}`);
});六、源码解析
以mysql2库的参数化查询为例,其底层使用MySQL的预处理语句功能。当执行:
pool.query(
'SELECT * FROM users WHERE username = ?',
[username]
);实际上会生成:
SELECT * FROM users WHERE username = 'alice'其中'alice'会自动进行转义处理,即使输入包含特殊字符如' OR '1'='1,也会被正确转义为' OR '1'='1,从而避免注入。
七、进阶使用
1. 使用命名参数
pool.query(
'SELECT * FROM users WHERE username = :username',
{ username: username }
);2. 复杂查询构建
const { Op } = require('sequelize');
User.findAll({
where: {
[Op.or]: [
{ username: { [Op.like]: `%${search}%` } },
{ password: { [Op.like]: `%${search}%` } }
]
}
});3. 使用事务
async function transfer(from, to, amount) {
const t = await sequelize.transaction();
try {
await sequelize.query(
'UPDATE accounts SET balance = balance - ? WHERE id = ?',
[amount, from],
{ transaction: t }
);
await sequelize.query(
'UPDATE accounts SET balance = balance + ? WHERE id = ?',
[amount, to],
{ transaction: t }
);
await t.commit();
} catch (err) {
await t.rollback();
throw err;
}
}八、性能与工程实践
1. 性能优化
- 使用连接池(
mysql2的Pool) - 避免过度使用
SELECT *,只查询需要的字段 - 对常用查询建立索引
- 对参数化查询进行缓存(注意安全边界)
2. 索引优化
CREATE INDEX idx_username ON users(username);3. 安全实践
- 使用最小权限原则创建数据库用户
- 禁用远程访问(除必要外)
- 定期更新数据库和驱动版本
- 使用
mysql2的escape方法处理特殊字符(不推荐)
九、常见问题与踩坑
1. 错误示例:拼接字符串
const sql = `SELECT * FROM users WHERE username = '${username}'`;问题:用户输入' OR '1'='1会触发注入
解决:改用参数化查询
2. 错误示例:正则替换特殊字符
const safe = username.replace(/[';]/g, '');问题:无法处理所有可能的注入方式
解决:使用参数化查询
3. 错误示例:使用mysql库的query方法
db.query("SELECT * FROM users WHERE username = '" + username + "'");问题:未使用参数化查询
解决:改用mysql2的参数化查询
4. 常见性能陷阱
- 不使用连接池导致频繁连接
- 未使用索引导致全表扫描
- 大量使用
SELECT *导致数据冗余
十、最佳实践
- 强制使用参数化查询:所有涉及用户输入的SQL语句必须使用参数化方式
- 使用ORM工具:如Sequelize、TypeORM等,可自动处理参数化
- 输入验证:对用户输入进行格式校验(如邮箱、手机号)
- 密码加密:使用
bcrypt、argon2等库处理密码存储 - 日志审计:记录所有数据库操作日志,便于安全审计
- 定期更新:保持数据库和驱动版本最新,修复已知漏洞
- 安全配置:设置合理的数据库用户权限,禁用远程访问
十一、总结
Node.js + MySQL防止SQL注入的核心在于参数化查询,通过将用户输入与SQL语句分离,避免恶意输入的注入攻击。本文详细介绍了多种实现方式,包括原始库的参数化查询、ORM工具的自动处理,以及在实际项目中的完整应用案例。
需要特别注意的是:参数化查询虽然安全,但需配合输入验证和密码加密等措施形成完整的安全体系。在实际开发中,应根据项目规模选择合适的方案,对于涉及敏感数据的系统,推荐使用ORM工具并严格遵循安全规范。
评论已关闭