WebRTC 安全性

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 。您应该看到以下内容−

WebRTC 安全性

现在打开一个新的终端窗口并运行 redis-cli 以打开一个客户端应用程序。

WebRTC 安全性

基本上,Redis是一个键值数据库。要创建具有字符串值的键,应该使用SET命令。要读取键值,应该使用GET命令。让我们为两个用户添加用户名和密码。键将是用户名,这些键的值将是相应的密码。

WebRTC 安全性

//require the redis library in Node.js 
var redis = require("redis");

//creating the redis client object 
var redisClient = redis.createClient();

在以上代码中,我们使用Node.js的Redis库并创建了一个Redis客户端用于我们的服务器。

要添加身份验证,请修改连接对象上的消息处理程序 –

//when a user connects to our sever 
wss.on('connection', function(connection) { 
   console.log("user connected");

   //when server gets a message from a connected user 
   connection.on('message', function(message) { 

      var data; 
      //accepting only JSON messages 
      try { 
         data = JSON.parse(message); 
      } catch (e) { 
         console.log("Invalid JSON"); 
         data = {}; 
      }

      //check whether a user is authenticated 
      if(data.type != "login") { 

         //if user is not authenticated 
         if(!connection.isAuth) { 
            sendTo(connection, { 
               type: "error", 
               message: "You are not authenticated" 
            }); 
            return; 
         } 
      } 

      //switching type of the user message 
      switch (data.type) { 
         //when a user tries to login 
         case "login": 
            console.log("User logged:", data.name); 
            //get password for this username from redis database 

            redisClient.get(data.name, function(err, reply) {  
               //check if password matches with the one stored in redis 
               var loginSuccess = reply === data.password;

               //if anyone is logged in with this username or incorrect password 
                  then refuse 
               if(users[data.name] || !loginSuccess) { 
                  sendTo(connection, { 
                     type: "login", 
                     success: false 
                  }); 
               } else { 
                  //save user connection on the server 
                  users[data.name] = connection; 
                  connection.name = data.name;
                  connection.isAuth = true; 

                  sendTo(connection, { 
                     type: "login", 
                     success: true 
                  }); 
               }  
            }); 

            break;
      }
   });

}       

//... 
//*****other handlers*******

在上面的代码中,如果用户尝试登录,我们从Redis获取他的密码,检查它是否与存储的密码匹配,如果成功,我们将他的用户名存储在服务器上。我们还会在连接中添加isAuth标志来检查用户是否经过身份验证。注意这段代码-

//check whether a user is authenticated 
if(data.type != "login") { 

   //if user is not authenticated 
   if(!connection.isAuth) { 
      sendTo(connection, { 
         type: "error", 
         message: "You are not authenticated" 
      });

      return; 
   } 
}

如果未经身份验证的用户尝试发送报价或离开连接,则会简单地发送一个错误回应。

下一步是启用安全套接字连接。这对于WebRTC应用程序是强烈推荐的。PKI(公钥基础设施)是来自CA(证书颁发机构)的数字签名。然后,用户检查用于签署证书的私钥与CA证书的公钥是否匹配。出于开发目的,我们将使用自签名的安全证书。

我们将使用openssl。它是一个实现SSL(安全套接字层)和TLS(传输层安全)协议的开源工具。它通常在Unix系统上默认安装。运行openssl version -a来检查是否安装了它。
WebRTC 安全性

为了生成公共和私有的安全证书密钥,您应该按照以下步骤进行:

  • 生成一个临时的服务器密码键
openssl genrsa -des3 -passout pass:x -out server.pass.key 2048

WebRTC 安全性

  • 生成服务器私钥
openssl rsa -passin pass:12345 -in server.pass.key -out server.key

WebRTC 安全性

  • 生成签名请求。有关您的公司,您将被要求回答其他问题。只需一直按下“Enter”按钮即可。
openssl req -new -key server.key -out server.csr

WebRTC 安全性

  • 生成证书
openssl x509 -req -days 1095 -in server.csr -signkey server.key -out server.crt

WebRTC 安全性

现在你有两个文件,证书(server.crt)和私钥(server.key)。请将它们复制到信令服务器的根目录。

为了启用安全套接字连接,请修改我们的信令服务器。

//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";

//out secure server will bind to the port 9090 
var cfg = { 
   port: 9090, 
   ssl_key: 'server.key', 
   ssl_cert: 'server.crt' 
};

//in case of http request just send back "OK" 
var processRequest = function(req, res) { 
   res.writeHead(200); 
   res.end("OK"); 
};

//create our server with SSL enabled 
var app = httpServ.createServer({ 
   key: fs.readFileSync(cfg.ssl_key), 
   cert: fs.readFileSync(cfg.ssl_cert) 
}, processRequest).listen(cfg.port);

//require our websocket library 
var WebSocketServer = require('ws').Server; 

//creating a websocket server at port 9090 
var wss = new WebSocketServer({server: app}); 

//all connected to the server users 
var users = {};

//require the redis library in Node.js
var redis = require("redis"); 

//creating the redis client object 
var redisClient = redis.createClient(); 

//when a user connects to our sever 
wss.on('connection', function(connection){ 
//...other code

在上述代码中,我们使用 fs 库来读取私钥和证书,并使用绑定端口和私钥、证书路径创建 cfg 对象。然后我们在端口号9090上创建一个HTTPS服务器,并在该端口上创建一个WebSocket服务器。

现在在Opera浏览器中打开 https://localhost:9090 。您应该看到以下内容 −

WebRTC 安全性

点击“继续”按钮。您应该看到“确定”消息。

为了测试我们的安全信令服务器,我们将修改我们在“WebRTC文本演示”教程中创建的聊天应用程序。我们只需要添加一个密码字段。以下是整个 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>

我们还需要通过以下代码在client.js文件中启用安全的套接字连接: var conn = new WebSocket(‘wss://localhost:9090’); 。 注意使用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文件的内容 –

//our username 
var name; 
var connectedUser;

//connecting to our signaling server 
var conn = new WebSocket('wss://localhost:9090');

conn.onopen = function () { 
   console.log("Connected to the signaling server"); 
};

//when we got a message from a signaling server 
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; 
      //when somebody wants to call us 
      case "offer":
         handleOffer(data.offer, data.name); 
         break; 
      case "answer": 
         handleAnswer(data.answer); 
         break; 
      //when a remote peer sends an ice candidate to us 
      case "candidate": 
         handleCandidate(data.candidate); 
         break; 
      case "leave": 
         handleLeave(); 
         break; 
      default: 
         break; 
   } 
};

conn.onerror = function (err) { 
   console.log("Got error", err); 
};  

//alias for sending JSON encoded messages 
function send(message) { 
   //attach the other peer username to our messages 
   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";

// Login when the user clicks the button 
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";

      //********************** 
      //Starting a peer connection 
      //********************** 

      //using Google public stun server 
      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); 
      };

      //when we receive a message from the other peer, display it on the screen 
      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"); 
      });  
   } 
});

//when somebody sends us an offer 
function handleOffer(offer, name) { 
   connectedUser = name; 
   yourConn.setRemoteDescription(new RTCSessionDescription(offer));

   //create an answer to an offer 
   yourConn.createAnswer(function (answer) { 
      yourConn.setLocalDescription(answer); 

      send({ 
         type: "answer", 
         answer: answer 
      }); 

   }, function (error) { 
      alert("Error when creating an answer"); 
   }); 

};

//when we got an answer from a remote user 
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; 
}; 

//when user clicks the "send message" button 
sendMsgBtn.addEventListener("click", function (event) { 
   var val = msgInput.value; 
   chatArea.innerHTML += name + ": " + val + "<br />"; 

   //sending a message to a connected peer 
   dataChannel.send(val); 
   msgInput.value = ""; 
});

现在通过以下命令运行我们的安全信令服务器: node server 在修改后的聊天示例文件夹中运行 node static 在两个浏览器标签中打开 localhost:8080 尝试登录。请记住,只允许使用用户名为“user1”和密码为“password1”的用户以及用户名为“user2”和密码为“password2”的用户登录。然后建立RTCPeerConnection(呼叫另一个用户)并尝试发送消息。

WebRTC 安全性

以下是我们安全信令服务器的全部代码 –

//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";

//out secure server will bind to the port 9090 
var cfg = { 
   port: 9090, 
   ssl_key: 'server.key', 
   ssl_cert: 'server.crt' 
};

//in case of http request just send back "OK" 
var processRequest = function(req, res){ 
   res.writeHead(200); 
   res.end("OK"); 
};

//create our server with SSL enabled 
var app = httpServ.createServer({ 
   key: fs.readFileSync(cfg.ssl_key), 
   cert: fs.readFileSync(cfg.ssl_cert) 
}, processRequest).listen(cfg.port);

//require our websocket library 
var WebSocketServer = require('ws').Server; 

//creating a websocket server at port 9090 
var wss = new WebSocketServer({server: app}); 

//all connected to the server users 
var users = {};

//require the redis library in Node.js 
var redis = require("redis"); 

//creating the redis client object 
var redisClient = redis.createClient();

//when a user connects to our sever 
wss.on('connection', function(connection) { 
   console.log("user connected"); 

   //when server gets a message from a connected user 
   connection.on('message', function(message) {  

      var data; 
      //accepting only JSON messages 
      try { 
         data = JSON.parse(message); 
      } catch (e) { 
         console.log("Invalid JSON"); 
         data = {}; 
      } 

      //check whether a user is authenticated 
      if(data.type != "login") { 
         //if user is not authenticated 
         if(!connection.isAuth) { 
            sendTo(connection, { 
               type: "error", 
               message: "You are not authenticated" 
            }); 

            return; 
         } 
      }

      //switching type of the user message 
      switch (data.type) { 
         //when a user tries to login 
         case "login":
            console.log("User logged:", data.name); 
            //get password for this username from redis database 
            redisClient.get(data.name, function(err, reply) {

               //check if password matches with the one stored in redis 
               var loginSuccess = reply === data.password;

               //if anyone is logged in with this username or incorrect password 
                  then refuse 
               if(users[data.name] || !loginSuccess) { 
                  sendTo(connection, { 
                     type: "login", 
                     success: false 
                  }); 
               } else { 
                  //save user connection on the server 
                  users[data.name] = connection; 
                  connection.name = data.name; 
                  connection.isAuth = true; 

                  sendTo(connection, { 
                     type: "login", 
                     success: true 
                  }); 
               }  
            }); 

            break;

         case "offer": 
            //for ex. UserA wants to call UserB 
            console.log("Sending offer to: ", data.name); 

            //if UserB exists then send him offer details 
            var conn = users[data.name];

            if(conn != null) { 
               //setting that UserA connected with 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); 
            //for ex. UserB answers 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; 

            //notify the other user so he can disconnect his peer connection 
            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; 
      }  
   });

   //when user exits, for example closes a browser window 
   //this may help if we are still in "offer","answer" or "candidate" state 
   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应用程序范围内使用它们。

Python教程

Java教程

Web教程

数据库教程

图形图像教程

大数据教程

开发工具教程

计算机教程