房间密码保护:创建受保护房间、密码验证、密码加密存储
聊到房间密码保护,我得先说说自己的经历。早期做WebRTC项目时,我天真地以为只要房间ID够随机就安全了。结果呢?有用户写了个脚本,遍历房间号直接闯进了别人的视频会议。那场面,尴尬得我到现在还记得。从那以后,密码保护就成了我信令服务器的标配功能。
说白了,房间密码保护就是给房间加把锁。但这把锁怎么设计,怎么验证,怎么存密码,每一步都有讲究。今天咱们就把这三个环节拆开揉碎了讲清楚。
1. 创建受保护房间
先看房间创建流程。用户发起创建房间请求时,需要额外传一个密码字段。服务端收到后,不能直接存明文——这是底线。我习惯的做法是:先对密码做哈希处理,再把哈希值存到房间信息里。
来看代码实现:
// 房间创建接口
app.post('/api/rooms', async (req, res) => {
const { roomId, password } = req.body;
// 参数校验
if (!roomId || !password) {
return res.status(400).json({ error: '房间ID和密码不能为空' });
}
// 检查房间是否已存在
if (rooms.has(roomId)) {
return res.status(409).json({ error: '房间已存在' });
}
// 对密码进行哈希处理
const salt = crypto.randomBytes(16).toString('hex');
const hashedPassword = await hashPassword(password, salt);
// 创建房间对象,存储哈希值而非明文
const room = {
id: roomId,
passwordHash: hashedPassword,
salt: salt,
createdAt: Date.now(),
members: []
};
rooms.set(roomId, room);
res.json({
success: true,
roomId: roomId,
message: '受保护房间创建成功'
});
});
核心要点:永远不要存储明文密码。使用加盐哈希,每次创建房间生成随机盐值。这样即使数据库泄露,攻击者也拿不到原始密码。
2. 密码验证流程
用户加入受保护房间时,需要先通过密码验证。这个流程我设计成了两步:先验证密码,再建立WebRTC连接。
为什么会这样设计?你想啊,如果先建立连接再验证密码,那恶意用户就能通过连接信息推断出房间的存在。先验证再连接,从源头上杜绝了信息泄露。
验证流程如下:
// 密码验证接口
app.post('/api/rooms/:roomId/verify', async (req, res) => {
const { roomId } = req.params;
const { password } = req.body;
const room = rooms.get(roomId);
if (!room) {
return res.status(404).json({ error: '房间不存在' });
}
// 验证密码
const isValid = await verifyPassword(password, room.salt, room.passwordHash);
if (!isValid) {
return res.status(401).json({ error: '密码错误' });
}
// 生成临时令牌,用于后续加入房间
const token = generateToken(roomId, req.ip);
res.json({
success: true,
token: token,
expiresIn: 300 // 令牌5分钟有效
});
});
我的经验:验证成功后返回一个临时令牌,而不是直接允许加入。令牌有过期时间,可以防止重放攻击。我在生产环境中把令牌有效期设为5分钟,足够用户完成加入操作,又不会留下太长的攻击窗口。
3. 密码加密存储方案
密码怎么存?这个问题我踩过坑。早期我用的是简单的SHA256,后来发现彩虹表攻击分分钟就能破解。现在我的标准方案是:bcrypt 或 scrypt。
这两种算法都是专门为密码存储设计的,特点是计算速度慢——慢反而是好事,因为攻击者暴力破解的成本会大幅提高。
来看具体的哈希函数实现:
const crypto = require('crypto');
const util = require('util');
// 使用PBKDF2进行密码哈希
async function hashPassword(password, salt) {
return new Promise((resolve, reject) => {
crypto.pbkdf2(password, salt, 100000, 64, 'sha512', (err, derivedKey) => {
if (err) reject(err);
resolve(derivedKey.toString('hex'));
});
});
}
// 密码验证
async function verifyPassword(password, salt, storedHash) {
const hash = await hashPassword(password, salt);
return hash === storedHash;
}
// 生成随机令牌
function generateToken(roomId, ip) {
const data = `${roomId}:${ip}:${Date.now()}:${crypto.randomBytes(8).toString('hex')}`;
return crypto.createHash('sha256').update(data).digest('hex');
}
注意:迭代次数(100000)不是随便写的。太少了不安全,太多了影响性能。我一般根据服务器性能做压测,找到合适的平衡点。普通服务器100000次迭代大概需要50-100ms,这个延迟可以接受。
核心逻辑流程图
下面这张图展示了房间密码保护的完整流程,从创建到验证再到加入:
密码策略对比
不同的加密方案各有优劣。我整理了一张对比表,方便你根据项目需求选择:
| 方案 | 安全性 | 性能 | 推荐场景 |
|---|---|---|---|
| 明文存储 | 极低 | 最快 | 绝对不要用 |
| MD5/SHA1 | 低 | 快 | 不推荐,彩虹表可破解 |
| SHA256 + 盐值 | 中等 | 较快 | 小型项目,非核心数据 |
| PBKDF2 | 高 | 较慢(可控) | Web应用,推荐使用 |
| bcrypt/scrypt | 极高 | 慢 | 高安全需求,推荐使用 |
我的建议:如果项目刚起步,用PBKDF2就足够了。等用户量上来了,再考虑迁移到bcrypt。我在一个日活10万的项目里用的就是PBKDF2,迭代次数设成50000,既安全又不会拖慢响应速度。
避坑指南
做密码保护时,有几个坑我替你们踩过了:
- 不要自己造哈希算法——我曾经见过有人把密码倒序再拼接时间戳,以为这样很安全。结果呢?攻击者一眼就看穿了。用标准库,别折腾。
- 密码长度限制要合理——我见过限制密码最多8位的系统。这等于告诉攻击者:我的密码很弱。建议最少8位,最长64位就够了。
- 验证接口要做限流——不加限流的话,攻击者可以暴力穷举。我习惯用IP加房间ID做双重限流,每分钟最多尝试5次。
- 日志里别打印密码——这个错误我犯过一次。调试时顺手把请求体打到了日志里,结果密码全暴露了。从那以后,所有密码字段在日志里都用星号代替。
重要提醒:密码验证接口返回的错误信息不要太具体。不要说「密码错误」或「用户不存在」,统一返回「验证失败」。否则攻击者能通过错误信息推断出哪些房间存在,哪些不存在。
嗯,房间密码保护这块就讲到这里。核心思路就是:创建时加密存储,加入时验证哈希,过程中做好限流和日志保护。把这些都做到位了,你的信令服务器在安全方面就及格了。