房间密码保护:创建受保护房间、密码验证、密码加密存储

聊到房间密码保护,我得先说说自己的经历。早期做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,后来发现彩虹表攻击分分钟就能破解。现在我的标准方案是:bcryptscrypt

这两种算法都是专门为密码存储设计的,特点是计算速度慢——慢反而是好事,因为攻击者暴力破解的成本会大幅提高。

来看具体的哈希函数实现:

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,这个延迟可以接受。

核心逻辑流程图

下面这张图展示了房间密码保护的完整流程,从创建到验证再到加入:

房间密码保护核心流程 1. 创建受保护房间 用户提交 roomId + password 2. 密码加密存储 生成盐值 → PBKDF2哈希 3. 存储哈希值 房间信息 + 哈希 4. 用户请求加入 提交 roomId + password 5. 密码验证 重新哈希 → 对比存储值 验证 验证失败 返回 401 错误 6. 生成临时令牌 有效期5分钟 验证成功 允许加入房间 密码保护流程:创建 → 加密存储 → 验证 → 授权加入

密码策略对比

不同的加密方案各有优劣。我整理了一张对比表,方便你根据项目需求选择:

方案 安全性 性能 推荐场景
明文存储 极低 最快 绝对不要用
MD5/SHA1 不推荐,彩虹表可破解
SHA256 + 盐值 中等 较快 小型项目,非核心数据
PBKDF2 较慢(可控) Web应用,推荐使用
bcrypt/scrypt 极高 高安全需求,推荐使用

我的建议:如果项目刚起步,用PBKDF2就足够了。等用户量上来了,再考虑迁移到bcrypt。我在一个日活10万的项目里用的就是PBKDF2,迭代次数设成50000,既安全又不会拖慢响应速度。

避坑指南

做密码保护时,有几个坑我替你们踩过了:

  • 不要自己造哈希算法——我曾经见过有人把密码倒序再拼接时间戳,以为这样很安全。结果呢?攻击者一眼就看穿了。用标准库,别折腾。
  • 密码长度限制要合理——我见过限制密码最多8位的系统。这等于告诉攻击者:我的密码很弱。建议最少8位,最长64位就够了。
  • 验证接口要做限流——不加限流的话,攻击者可以暴力穷举。我习惯用IP加房间ID做双重限流,每分钟最多尝试5次。
  • 日志里别打印密码——这个错误我犯过一次。调试时顺手把请求体打到了日志里,结果密码全暴露了。从那以后,所有密码字段在日志里都用星号代替。

重要提醒:密码验证接口返回的错误信息不要太具体。不要说「密码错误」或「用户不存在」,统一返回「验证失败」。否则攻击者能通过错误信息推断出哪些房间存在,哪些不存在。

嗯,房间密码保护这块就讲到这里。核心思路就是:创建时加密存储,加入时验证哈希,过程中做好限流和日志保护。把这些都做到位了,你的信令服务器在安全方面就及格了。

公众号:蓝海资料掘金营,微信deep3321