数据库安全
问题
数据库安全需要关注哪些方面?如何防止 SQL 注入?
答案
安全威胁与防御
| 威胁 | 防御措施 |
|---|---|
| SQL 注入 | 参数化查询 / ORM |
| 未授权访问 | 最小权限原则 |
| 数据泄露 | 加密存储、脱敏 |
| 备份泄露 | 备份加密 |
| 内部攻击 | 审计日志、RLS |
SQL 注入防御
sql-injection-prevention.ts
// ❌ 拼接 SQL(极度危险)
const query = `SELECT * FROM users WHERE email = '${email}'`;
// ✅ 参数化查询
const [rows] = await connection.execute(
'SELECT * FROM users WHERE email = ?',
[email],
);
// ✅ ORM 自动防注入
const user = await prisma.user.findUnique({
where: { email }, // Prisma 自动参数化
});
// ✅ Prisma 原生查询也安全(模板标签)
const user = await prisma.$queryRaw`
SELECT * FROM users WHERE email = ${email}
`;
// 注意:不要用 prisma.$queryRawUnsafe()
行级安全(RLS)
rls.sql
-- PostgreSQL 行级安全策略
-- 用户只能看到自己的数据
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
-- 创建策略
CREATE POLICY user_orders ON orders
FOR ALL
USING (user_id = current_setting('app.current_user_id')::int);
-- 应用中设置当前用户
SET app.current_user_id = '123';
SELECT * FROM orders; -- 自动只返回 user_id = 123 的数据
数据加密
encryption.ts
import crypto from 'crypto';
const ALGORITHM = 'aes-256-gcm';
const KEY = Buffer.from(process.env.ENCRYPTION_KEY!, 'hex'); // 32 bytes
function encrypt(text: string): string {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(ALGORITHM, KEY, iv);
const encrypted = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()]);
const tag = cipher.getAuthTag();
return `${iv.toString('hex')}:${tag.toString('hex')}:${encrypted.toString('hex')}`;
}
function decrypt(data: string): string {
const [ivHex, tagHex, encryptedHex] = data.split(':');
const decipher = crypto.createDecipheriv(ALGORITHM, KEY, Buffer.from(ivHex, 'hex'));
decipher.setAuthTag(Buffer.from(tagHex, 'hex'));
return decipher.update(encryptedHex, 'hex', 'utf8') + decipher.final('utf8');
}
// 敏感数据加密存储
await prisma.user.create({
data: {
name: 'Alice',
idCard: encrypt('110101199001011234'), // 加密身份证号
},
});
常见面试问题
Q1: 最小权限原则怎么实践?
答案:
- 应用使用专用数据库用户,只授予必要的 SELECT/INSERT/UPDATE 权限
- 不给应用 DROP/ALTER/GRANT 权限
- 生产数据库禁止直接登录,通过审计工具操作
Q2: 密码应该怎么存储?
答案:
使用 bcrypt(自动加盐,防彩虹表):
import bcrypt from 'bcrypt';
const hash = await bcrypt.hash(password, 12);
const match = await bcrypt.compare(inputPassword, hash);
永远不存明文,不用 MD5/SHA256(无盐易被破解)。
Q3: 数据脱敏怎么做?
答案:
// 手机号:138****8000
function maskPhone(phone: string): string {
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
}
// 身份证:110***********1234
function maskIdCard(id: string): string {
return id.replace(/^(.{3})(.+)(.{4})$/, '$1' + '*'.repeat(11) + '$3');
}
相关链接
- 服务端安全 - 服务端安全全景
- Node.js 安全 - Node.js 安全实践