跳到主要内容

数据库安全

问题

数据库安全需要关注哪些方面?如何防止 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');
}

相关链接