Skip to content

WebRTC - 安全

WebRTC 本身在设计时就将安全放在首位。所有 WebRTC 组件,包括媒体流(media streams)和数据通道(data channels),都强制加密。媒体流使用 SRTP(Secure Real-time Transport Protocol,安全实时传输协议)加密,数据通道和密钥交换使用 DTLS(Datagram Transport Layer Security,数据报传输层安全协议)加密。本章重点关注如何增强信令服务器(signaling server)和应用环境(application environment)的安全性,它们是 WebRTC 部署中至关重要的辅助部分。

我们将探讨两种主要的增强措施:

  • 为信令服务器添加用户身份验证,例如使用 Redis 数据库。
  • 为信令启用安全的 WebSocket 连接 (WSS)。

使用 Redis 实现信令服务器用户身份验证

Section titled “使用 Redis 实现信令服务器用户身份验证”

在使用信令服务器之前对用户进行身份验证有助于防止未经授权的访问和滥用。虽然可以使用各种数据库,但本例使用 Redis,一个快速的内存键值存储系统。

设置 Redis 用于开发:

  • 安装 Redis:您可以从 redis.io 下载并从源代码编译,或使用包管理器(例如,在 Debian/Ubuntu 上使用 sudo apt install redis-server,在 macOS 上使用 brew install redis)。对于快速的本地开发,Docker 也是一个很好的选择:docker run -d -p 6379:6379 --name my-redis redis。
  • 启动 Redis Server:如果安装程序没有自动启动,运行 redis-server。
  • 与 Redis 交互:使用 redis-cli 连接到您的 Redis 实例。您可以设置用户名和(哈希过的)密码。对于本演示,我们将存储明文密码,这在生产环境中是绝对不推荐的。

在 redis-cli 中,让我们添加一些示例用户(在实际应用中用您实际的用户管理逻辑替换):

SET user1:password mysecretpassword1
SET user2:password mysecretpassword2

原始教程展示了一张 redis-cli 的图片,其中包含 SET user1 password123 和 GET user1 命令。上面的命令实现了类似的结果,但使用了稍好一点的键命名约定。

现在,修改您的 Node.js 信令服务器(来自之前的演示)以包含身份验证。首先,安装 Redis 客户端库:npm install redis。

// server.js (partial - Redis integration)
const WebSocket = require('ws');
const redis = require('redis'); // npm install redis
// ... (WebSocket server setup as before) ...
// Create Redis client (v4+ of the library)
const redisClient = redis.createClient(); // Connects to redis://localhost:6379 by default
(async () => {
redisClient.on('error', (err) => console.error('Redis Client Error', err));
try {
await redisClient.connect();
console.log('Connected to Redis successfully!');
} catch (err) {
console.error('Could not connect to Redis:', err);
process.exit(1); // Exit if Redis connection fails, as it's critical for auth
}
})();
// ... (users object) ...
wss.on('connection', (connection) => {
console.log('User connected, awaiting authentication');
connection.isAuthenticated = false; // Custom flag
connection.on('message', async (message) => { // Make handler async for Redis
let data;
try {
data = JSON.parse(message);
} catch (e) { /* ... */ return; }
// All messages except 'login' require authentication
if (data.type !== 'login' && !connection.isAuthenticated) {
sendTo(connection, { type: 'error', message: 'Not authenticated' });
return;
}
switch (data.type) {
case 'login':
// console.log(`Login attempt by: ${data.name}`);
if (!data.name || !data.password) {
sendTo(connection, { type: 'login', success: false, message: 'Username and password required.' });
return;
}
try {
const storedPassword = await redisClient.get(`${data.name}:password`);
if (storedPassword === data.password) {
if (users[data.name]) {
sendTo(connection, { type: 'login', success: false, message: 'User already logged in elsewhere.' });
} else {
users[data.name] = connection;
connection.name = data.name;
connection.isAuthenticated = true;
sendTo(connection, { type: 'login', success: true });
console.log(`User ${data.name} authenticated and logged in.`);
}
} else {
sendTo(connection, { type: 'login', success: false, message: 'Invalid username or password.' });
}
} catch (err) {
console.error('Redis error during login:', err);
sendTo(connection, { type: 'login', success: false, message: 'Server error during login.' });
}
break;
// ... other cases (offer, answer, candidate, leave) remain largely the same ...
// Ensure they check connection.isAuthenticated if needed, though the top check handles it.
default:
// ...
break;
}
});
connection.on('close', () => {
// ... (handleDisconnect logic as before, ensuring isAuthenticated state is considered if needed)
if (connection.name && connection.isAuthenticated) {
console.log(`User ${connection.name} disconnected.`);
delete users[connection.name];
// Notify otherName if in a call
}
});
// ... (error handler) ...
});
function sendTo(conn, message) { /* ... as before ... */ }
// ... (other handlers: handleOffer, handleAnswer, etc.) ...

身份验证的关键变化:

  • 导入 redis 库,并创建和连接客户端(使用现代的 async/await)。
  • 添加了一个 connection.isAuthenticated 标志。
  • login 情况现在查询 Redis 获取与用户名关联的密码并进行比较。
  • 重要提示:存储和比较明文密码存在重大的安全风险。在生产系统中,您必须安全地对密码进行哈希处理(例如,使用 bcrypt 或 Argon2)并存储哈希值。

信令消息即使不包含媒体,也可能携带敏感信息(例如,如果不使用 mDNS,则 ICE 候选者中可能包含 IP 地址,或用户标识符)。强烈建议使用 WSS(WebSocket Secure,即基于 TLS/SSL 的 WebSocket)对信令通道进行加密。这需要一个 HTTPS 服务器。

对于开发环境,您可以使用自签名 SSL 证书。对于生产环境,请从受信任的证书颁发机构 (CA) 获取证书(例如,Let’s Encrypt 提供免费证书)。

使用 OpenSSL 生成自签名证书(通常预装在 Linux/macOS 上,也适用于 Windows):

  1. 生成私钥:openssl genrsa -out server.key 2048
  2. 生成证书签名请求 (CSR):openssl req -new -key server.key -out server.csr(系统会提示您输入信息;对于本地开发,使用默认值即可,测试本地时请确保通用名 Common Name 是 localhost)。
  3. 生成自签名证书:openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt

原始教程详细介绍了密钥生成的更多中间步骤(例如,带有密码短语)。上述方法是用于基本自签名证书的更直接的方法。这些命令将创建 server.key(私钥)和 server.crt(证书)。将它们放在您的服务器根文件夹中。

修改信令服务器以使用 HTTPS 和 WSS:

// server.js (partial - WSS integration)
const https = require('https');
const fs = require('fs');
const WebSocket = require('ws');
// const redis = require('redis'); // and Redis client setup if using authentication
// SSL Configuration
const privateKey = fs.readFileSync('server.key', 'utf8');
const certificate = fs.readFileSync('server.crt', 'utf8');
const credentials = { key: privateKey, cert: certificate };
// Create HTTPS server
const httpsServer = https.createServer(credentials, (req, res) => {
// Basic HTTP handler (e.g., for health checks or serving client files)
res.writeHead(200);
res.end('HTTPS Server for WebRTC Signaling\n');
});
// Create WebSocket server and attach it to the HTTPS server
const wss = new WebSocket.Server({ server: httpsServer });
const port = 9090;
httpsServer.listen(port, () => {
console.log(`Secure Signaling server started on wss://localhost:${port}`);
});
// ... (rest of the WebSocket and Redis logic: wss.on('connection', ...), etc.) ...
// Note: For development with self-signed certificates, browsers will show a warning.
// You'll need to accept the risk to proceed. Some clients might require specific flags
// to allow self-signed certs (e.g., Node.js `NODE_TLS_REJECT_UNAUTHORIZED=0` for a Node client,
// but this is NOT for browser clients). Browsers usually provide an option to bypass the warning.

WSS 的关键变化:

  • 使用了 https 和 fs 模块。
  • 读取了 SSL 密钥和证书文件。
  • 实例化了一个带有这些凭据的 https.createServer。
  • 然后将 WebSocket.Server 配置为使用此 httpsServer,而不是直接绑定到端口。
  • 服务器现在监听 wss://localhost:9090。

如果您已保护了信令服务器,客户端应用(例如,来自文本演示或语音演示)需要进行一些更改:

  1. 连接到 WSS:将 WebSocket URL 从 ws:// 更改为 wss://:
  2. // client.js // const wsURL = ‘ws://localhost:9090’; // Old const wsURL = ‘wss://localhost:9090’; // New for secure connection const connection = new WebSocket(wsURL);
  3. 登录时发送密码:如果启用了身份验证,请修改登录部分以发送密码。index.html 需要一个密码输入字段:
5. // client.js (updated login logic) const passwordInput = document.querySelector('#passwordInput'); // Add selector

loginBtn.addEventListener(‘click’, () => { name = usernameInput.value; const password = passwordInput.value; // Get password if (name.length > 0 && password.length > 0) { send({ type: ‘login’, name: name, password: password // Send password }); } else { alert(‘Please enter both username and password.’); } });

原始教程提供了一个带有这些更改的完整 client.js 示例,但它错误地将 JavaScript 代码包装在 <pre> 块内的 <html><body><p> 标签中。正确的方法是仅显示 JavaScript 代码,如上面代码片段所示。

当客户端连接到带有自签名证书的服务器时,您的浏览器会显示安全警告(例如,“您的连接不是私密的”)。您需要接受风险才能继续(例如,“高级”->“继续前往 localhost (不安全)”)进行测试。自签名证书会显示此警告是正常的。

原始教程中的一张图片显示了无效证书的浏览器警告,以及继续后显示的“OK”消息,这是基本 HTTPS 服务器的响应。使用我们的完整 WSS 服务器,客户端应用在绕过警告后应该能够连接并正常工作。

  • 应用层(Application Layer):对信令用户进行身份验证以控制访问。
  • 信令传输层(Signaling Transport Layer):使用 WSS(基于 HTTPS 的 WebSockets)加密客户端和服务器之间的信令消息。
  • WebRTC 媒体/数据层(WebRTC Media/Data Layer):这层本质上是安全的。媒体流使用 SRTP 加密,数据通道使用 DTLS 加密。密钥使用 DTLS-SRTP 安全地交换。浏览器强制执行此操作。

通过为您的信令服务器实现身份验证并使用安全的 WebSockets (WSS),您可以显著增强 WebRTC 应用的整体安全性。请始终记住在生产部署中使用强大的密码哈希算法并从 CA 获取有效的 SSL 证书。