WebRTC - 安全
在本章中,我们将为我们在"WebRTC 信令"一章中创建的信令服务器添加安全功能。将会有两个增强功能 −
- 使用 Redis 数据库进行用户身份验证
- 启用安全套接字连接
首先,您应该安装 Redis。
在 http://redis.io/download 下载最新稳定版本(我的情况是 3.05)
解压
在下载的文件夹中运行 sudo make install
安装完成后,运行 make test 检查一切是否正常运行。
Redis 有两个可执行命令 −
redis-cli − Redis 的命令行界面(客户端部分)
redis-server − Redis 数据存储
要运行 Redis 服务器,请在终端控制台中输入 redis-server。您应该看到以下 −
现在打开一个新的终端窗口并运行 redis-cli 以打开客户端应用程序。
基本上,Redis 是一个键值数据库。要创建具有字符串值的键,您应该使用 SET 命令。要读取密钥值,您应该使用 GET 命令。让我们为它们添加两个用户和密码。密钥将是用户名,这些密钥的值将是相应的密码。
现在我们应该修改我们的信令服务器以添加用户身份验证。将以下代码添加到 server.js 文件的顶部 −
//在 Node.js 中需要 redis 库
var redis = require("redis");
//创建 redis 客户端对象
var redisClient = redis.createClient();
在上面的代码中,我们需要 Node.js 的 Redis 库并为我们的服务器创建一个 redis 客户端。
要添加身份验证,请修改连接对象上的 message 处理程序 −
//当用户连接到我们的服务器时
wss.on('connection', function(connection) {
console.log("user connected");
//当服务器收到来自已连接用户的消息时
connection.on('message', function(message) {
var data;
//仅接受 JSON 消息
try {
data = JSON.parse(message);
} catch (e) {
console.log("Invalid JSON");
data = {};
}
//检查用户是否已经通过身份验证
if(data.type != "login") {
//如果用户没有经过身份验证
if(!connection.isAuth) {
sendTo(connection, {
type: "error",
message: "You are not authenticated"
});
return;
}
}
//切换用户消息类型
switch (data.type) {
//当用户尝试登录时
case "login":
console.log("User logged:", data.name);
//从 redis 数据库获取此用户名的密码
redisClient.get(data.name, function(err, reply) {
//检查密码是否与redis中存储的密码匹配
var loginSuccess = reply === data.password;
//如果有人用这个用户名或错误的密码登录
then refuse
if(users[data.name] || !loginSuccess) {
sendTo(connection, {
type: "login",
success: false
});
} else {
//在服务器上保存用户连接
users[data.name] = connection;
connection.name = data.name;
connection.isAuth = true;
sendTo(connection, {
type: "login",
success: true
});
}
});
break;
}
});
}
//...
//*****other handlers*******
在上面的代码中,如果用户尝试登录,我们会从 Redis 获取他的密码,检查它是否与存储的密码匹配,如果成功,我们会将他的用户名存储在服务器上。我们还将 isAuth 标志添加到连接以检查用户是否经过身份验证。请注意此代码 −
//检查用户是否已经通过身份验证
if(data.type != "login") {
//如果用户没有经过身份验证
if(!connection.isAuth) {
sendTo(connection, {
type: "error",
message: "You are not authenticated"
});
return;
}
}
如果未经身份验证的用户尝试发送要约或离开连接,我们只会发回错误。
下一步是启用安全套接字连接。强烈建议 WebRTC 应用程序使用安全套接字连接。PKI(公钥基础设施)是来自 CA(证书颁发机构)的数字签名。然后,用户检查用于签署证书的私钥是否与 CA 证书的公钥匹配。出于开发目的,我们将使用自签名安全证书。
我们将使用 openssl。它是一个实现 SSL(安全套接字层)和 TLS(传输层安全性)协议的开源工具。它通常默认安装在 Unix 系统上。运行 openssl version -a 检查是否已安装。
要生成公钥和私钥安全证书密钥,请按照以下步骤操作 −
生成临时服务器密码密钥
openssl genrsa -des3 -passout pass:x -out server.pass.key 2048
生成服务器私钥
openssl rsa -passin pass:12345 -in server.pass.key -out server.key
生成签名请求。系统会询问您有关公司的其他问题。只需一直按"Enter"键即可。
openssl req -new -key server.key -out server.csr
生成证书
openssl x509 -req -days 1095 -in server.csr -signkey server.key -out server.crt
现在您有两个文件,证书(server.crt)和私钥(server.key)。将它们复制到信令服务器根文件夹中。
要启用安全套接字连接,请修改我们的信令服务器。
//需要文件系统模块
var fs = require('fs');
var httpServ = require('https');
//https://github.com/visionmedia/superagent/issues/205
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
//安全服务器将绑定到端口 9090
var cfg = {
port: 9090,
ssl_key: 'server.key',
ssl_cert: 'server.crt'
};
//如果是 http 请求,只需返回"OK"
var processRequest = function(req, res) {
res.writeHead(200);
res.end("OK");
};
//创建启用 SSL 的服务器
var app = httpServ.createServer({
key: fs.readFileSync(cfg.ssl_key),
cert: fs.readFileSync(cfg.ssl_cert)
}, processRequest).listen(cfg.port);
//需要我们的 websocket 库
var WebSocketServer = require('ws').Server;
//在端口 9090 处创建一个 websocket 服务器
var wss = new WebSocketServer({server: app});
//所有连接到服务器的用户
var users = {};
//需要 Node.js 中的 redis 库
var redis = require("redis");
//创建 redis 客户端对象
var redisClient = redis.createClient();
//当用户连接到我们的服务器时
wss.on('connection', function(connection){
//...其他代码
在上面的代码中,我们需要 fs 库来读取私钥和证书,使用私钥和证书的绑定端口和路径创建 cfg 对象。然后,我们使用密钥创建一个 HTTPS 服务器,并在端口 9090 上创建一个 WebSocket 服务器。
现在在 Opera 中打开 https://localhost:9090。您应该看到以下 −
单击"仍然继续"按钮。您应该看到"OK"消息。
为了测试我们的安全信令服务器,我们将修改我们在"WebRTC Text Demo"教程中创建的聊天应用程序。我们只需要添加一个密码字段。以下是整个 index.html 文件 −
<html>
<head>
<title>WebRTC Text Demo</title>
<link rel = "stylesheet" href = "node_modules/bootstrap/dist/css/bootstrap.min.css"/>
</head>
<style>
body {
background: #eee;
padding: 5% 0;
}
</style>
<body>
<div id = "loginPage" class = "container text-center">
<div class = "row">
<div class = "col-md-4 col-md-offset-4">
<h2>WebRTC Text Demo. Please sign in</h2>
<label for = "usernameInput" class = "sr-only">Login</label>
<input type = "email" id = "usernameInput"
class = "form-control formgroup" placeholder = "Login"
required = "" autofocus = "">
<input type = "text" id = "passwordInput"
class = "form-control form-group" placeholder = "Password"
required = "" autofocus = "">
<button id = "loginBtn" class = "btn btn-lg btn-primary btnblock"
>Sign in</button>
</div>
</div>
</div>
<div id = "callPage" class = "call-page container">
<div class = "row">
<div class = "col-md-4 col-md-offset-4 text-center">
<div class = "panel panel-primary">
<div class = "panel-heading">Text chat</div>
<div id = "chatarea" class = "panel-body text-left"></div>
</div>
</div>
</div>
<div class = "row text-center form-group">
<div class = "col-md-12">
<input id = "callToUsernameInput" type = "text"
placeholder = "username to call" />
<button id = "callBtn" class = "btn-success btn">Call</button>
<button id = "hangUpBtn" class = "btn-danger btn">Hang Up</button>
</div>
</div>
<div class = "row text-center">
<div class = "col-md-12">
<input id = "msgInput" type = "text" placeholder = "message" />
<button id = "sendMsgBtn" class = "btn-success btn">Send</button>
</div>
</div>
</div>
<script src = "client.js"></script>
</body>
</html>
我们还需要通过此行 var conn = new WebSocket('wss://localhost:9090'); 在 client.js 文件中启用安全套接字连接。请注意 wss 协议。然后,必须修改登录按钮处理程序以将密码与用户名一起发送 −
loginBtn.addEventListener("click", function (event) {
name = usernameInput.value;
var pwd = passwordInput.value;
if (name.length > 0) {
send({
type: "login",
name: name,
password: pwd
});
}
});
以下是整个 client.js 文件 −
//我们的用户名
var name;
var ConnectedUser;
//连接到我们的信令服务器
var conn = new WebSocket('wss://localhost:9090');
conn.onopen = function () {
console.log("已连接到信令服务器");
};
//当我们从信令服务器收到消息时
conn.onmessage = function (msg) {
console.log("Got message", msg.data);
var data = JSON.parse(msg.data);
switch(data.type) {
case "login":
handleLogin(data.success);
break;
//当有人想打电话给我们时
case "offer":
handleOffer(data.offer, data.name);
break;
case "answer":
handleAnswer(data.answer);
break;
//当远程对等方向我们发送 candidate 时
case "candidate":
handleCandidate(data.candidate);
break;
case "leave":
handleLeave();
break;
default:
break;
}
};
conn.onerror = function (err) {
console.log("Got error", err);
};
//发送 JSON 编码消息的别名
function send(message) {
//将其他对等用户名附加到我们的消息中
if (connectedUser) {
message.name = connectedUser;
}
conn.send(JSON.stringify(message));
};
//******
//UI selectors block
//******
var loginPage = document.querySelector('#loginPage');
var usernameInput = document.querySelector('#usernameInput');
var passwordInput = document.querySelector('#passwordInput');
var loginBtn = document.querySelector('#loginBtn');
var callPage = document.querySelector('#callPage');
var callToUsernameInput = document.querySelector('#callToUsernameInput');
var callBtn = document.querySelector('#callBtn');
var hangUpBtn = document.querySelector('#hangUpBtn');
var msgInput = document.querySelector('#msgInput');
var sendMsgBtn = document.querySelector('#sendMsgBtn');
var chatArea = document.querySelector('#chatarea');
var yourConn;
var dataChannel;
callPage.style.display = "none";
// 当用户点击按钮时登录
loginBtn.addEventListener("click", function (event) {
name = usernameInput.value;
var pwd = passwordInput.value;
if (name.length > 0) {
send({
type: "login",
name: name,
password: pwd
});
}
});
function handleLogin(success) {
if (success === false) {
alert("Ooops...incorrect username or password");
} else {
loginPage.style.display = "none";
callPage.style.display = "block";
//**********************
//启动对等连接
//**********************
//使用Google公共stun服务器
var configuration = {
"iceServers": [{ "url": "stun:stun2.1.google.com:19302" }]
};
yourConn = new webkitRTCPeerConnection(configuration, {optional: [{RtpDataChannels: true}]});
// Setup ice handling
yourConn.onicecandidate = function (event) {
if (event.candidate) {
send({
type: "candidate",
candidate: event.candidate
});
}
};
//creating data channel
dataChannel = yourConn.createDataChannel("channel1", {reliable:true});
dataChannel.onerror = function (error) {
console.log("Ooops...error:", error);
};
//当我们收到对方的消息时,将其显示在屏幕上
dataChannel.onmessage = function (event) {
chatArea.innerHTML += connectedUser + ": " + event.data + "<br />";
};
dataChannel.onclose = function () {
console.log("data channel is closed");
};
}
};
//initiating a call
callBtn.addEventListener("click", function () {
var callToUsername = callToUsernameInput.value;
if (callToUsername.length > 0) {
connectedUser = callToUsername;
// create an offer
yourConn.createOffer(function (offer) {
send({
type: "offer",
offer: offer
});
yourConn.setLocalDescription(offer);
}, function (error) {
alert("Error when creating an offer");
});
}
});
//当有人向我们发送 offer 时
function handleOffer(offer, name) {
connectedUser = name;
yourConn.setRemoteDescription(new RTCSessionDescription(offer));
//创建对 offer 的答复
yourConn.createAnswer(function (answer) {
yourConn.setLocalDescription(answer);
send({
type: "answer",
answer: answer
});
}, function (error) {
alert("Error when creating an answer");
});
};
//当我们收到远程用户的答复时
function handleAnswer(answer) {
yourConn.setRemoteDescription(new RTCSessionDescription(answer));
};
//when we got an ice candidate from a remote user
function handleCandidate(candidate) {
yourConn.addIceCandidate(new RTCIceCandidate(candidate));
};
//hang up
hangUpBtn.addEventListener("click", function () {
send({
type: "leave"
});
handleLeave();
});
function handleLeave() {
connectedUser = null;
yourConn.close();
yourConn.onicecandidate = null;
};
//当用户点击"发送消息"按钮时
sendMsgBtn.addEventListener("click", function (event) {
var val = msgInput.value;
chatArea.innerHTML += name + ": " + val + "<br />";
//向已连接的对等体发送消息
dataChannel.send(val);
msgInput.value = "";
});
现在通过 node server 运行我们的安全信令服务器。在修改后的聊天演示文件夹中运行 node static。在两个浏览器选项卡中打开 localhost:8080。尝试登录。请记住,只有"user1"和"password1"以及"user2"和"password2"才允许登录。然后建立 RTCPeerConnection(呼叫另一个用户)并尝试发送消息。
以下是我们安全信令服务器的完整代码 −
//require file system module
var fs = require('fs');
var httpServ = require('https');
//https://github.com/visionmedia/superagent/issues/205
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
//安全服务器将绑定到端口 9090
var cfg = {
port: 9090,
ssl_key: 'server.key',
ssl_cert: 'server.crt'
};
//如果是 http 请求,只需返回"OK"
var processRequest = function(req, res){
res.writeHead(200);
res.end("OK");
};
//创建启用 SSL 的服务器
var app = httpServ.createServer({
key: fs.readFileSync(cfg.ssl_key),
cert: fs.readFileSync(cfg.ssl_cert)
}, processRequest).listen(cfg.port);
//需要我们的 websocket 库
var WebSocketServer = require('ws').Server;
//在端口 9090 处创建一个 websocket 服务器
var wss = new WebSocketServer({server: app});
//所有连接到服务器的用户
var users = {};
//需要 Node.js 中的 redis 库
var redis = require("redis");
//创建 redis 客户端对象
var redisClient = redis.createClient();
//当用户连接到我们的服务器时
wss.on('connection', function(connection) {
console.log("user connected");
//当服务器收到来自已连接用户的消息时
connection.on('message', function(message) {
var data;
//仅接受 JSON 消息
try {
data = JSON.parse(message);
} catch (e) {
console.log("Invalid JSON");
data = {};
}
//检查用户是否已经通过身份验证
if(data.type != "login") {
//如果用户没有经过身份验证
if(!connection.isAuth) {
sendTo(connection, {
type: "error",
message: "You are not authenticated"
});
return;
}
}
//切换用户消息类型
switch (data.type) {
//当用户尝试登录时
case "login":
console.log("User logged:", data.name);
//从 redis 数据库获取此用户名的密码
redisClient.get(data.name, function(err, reply) {
//检查密码是否与redis中存储的密码匹配
var loginSuccess = reply === data.password;
//如果有人用这个用户名或错误的密码登录
then refuse
if(users[data.name] || !loginSuccess) {
sendTo(connection, {
type: "login",
success: false
});
} else {
//在服务器上保存用户连接
users[data.name] = connection;
connection.name = data.name;
connection.isAuth = true;
sendTo(connection, {
type: "login",
success: true
});
}
});
break;
case "offer":
//例如,用户 A 想要呼叫用户 B
console.log("Sending offer to: ", data.name);
//如果 UserB 存在则向他发送优惠详情
var conn = users[data.name];
if(conn != null) {
//设置UserA与UserB连接
connection.otherName = data.name;
sendTo(conn, {
type: "offer",
offer: data.offer,
name: connection.name
});
}
break;
case "answer":
console.log("Sending answer to: ", data.name);
//例如 UserB 回答 UserA
var conn = users[data.name];
if(conn != null) {
connection.otherName = data.name;
sendTo(conn, {
type: "answer",
answer: data.answer
});
}
break;
case "candidate":
console.log("Sending candidate to:",data.name);
var conn = users[data.name];
if(conn != null) {
sendTo(conn, {
type: "candidate",
candidate: data.candidate
});
}
break;
case "leave":
console.log("Disconnecting from", data.name);
var conn = users[data.name];
conn.otherName = null;
//通知其他用户,以便他可以断开他的对等连接
if(conn != null) {
sendTo(conn, {
type: "leave"
});
}
break;
connection.on("close", function() {
if(connection.name) {
delete users[connection.name];
if(connection.otherName) {
console.log("Disconnecting from ", connection.otherName);
var conn = users[connection.otherName];
conn.otherName = null;
if(conn != null) {
sendTo(conn, {
type: "leave"
});
}
}
}
});
default:
sendTo(connection, {
type: "error",
message: "Command no found: " + data.type
});
break;
}
});
//当用户退出时,例如关闭浏览器窗口
//如果我们仍处于"offer","answer" 或 "candidate" 状态,这可能会有所帮助
connection.on("close", function() {
if(connection.name) {
delete users[connection.name];
}
});
connection.send("Hello from server");
});
function sendTo(connection, message) {
connection.send(JSON.stringify(message));
}
总结
在本章中,我们为信令服务器添加了用户身份验证。我们还学习了如何创建自签名 SSL 证书并在 WebRTC 应用程序范围内使用它们。

