开发手册 欢迎您!
软件开发者资料库

WebRTC - 安全

WebRTC安全 - 从概述,架构,环境,MediaStream API,RTCPeerConnection API,RTCDataChannel API,发送消息,信令,浏览器支持,移动支持,视频演示,语音演示,文本演示,安全性开始学习WebRTC。

在本章中,我们将向我们在"WebRTC信令"一章中创建的信令服务器添加安全功能.将有两个增强功能 :

  • 使用Redis数据库进行用户身份验证

  • 启用安全套接字连接

首先,您应该安装Redis.

  • 在 http://redis.io/download 下载最新的稳定版本(3.05 in我的情况)

  • 打开包装

  • 在下载的文件夹中运行 sudo make install

  • 安装完成后,运行 make test 检查一切是否正常.

Redis有两个可执行命令 :

  • redis-cli :  Redis的命令行界面(客户端部分)

  • redis-server :  Redis数据存储

在终端控制台中运行Redis服务器类型 redis-server .您应该看到以下 :

Redis Server

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

Redis -cli

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

添加用户和密码

现在我们应该修改我们的信令服务器以添加用户身份验证.将以下代码添加到 server.js 文件的顶部 :

//require the redis library in Node.js var redis = require("redis"); //creating the redis client object var redisClient = redis.createClient();

在上面的代码中,我们需要Redis库用于Node.js并为我们的服务器创建一个redis客户端.

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

//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 以检查它是否已安装.

使用Openssl

要生成公共和私人安全证书密钥,您应该按照下面给出的步骤 : 去;

  • 生成临时服务器密码密钥

  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).将它们复制到信令服务器根文件夹中.

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

//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.jsvar 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上的WebSocket服务器创建一个HTTPS服务器.

现在打开 https://localhost在Opera中你应该看到以下内容:

无效证书

单击"仍然继续"按钮.您应该看到"确定"消息.

要测试我们的安全信令服务器,我们将修改我们在"WebRTC文本演示"教程.我们只需要添加一个密码字段.以下是整个 index.html 文件 :

            WebRTC Text Demo                                                                    

WebRTC Text Demo. Please sign in

                Login                                                Sign in              
          
                                                                       Text chat                                                                                                                Call                Hang Up                                                                             Send                                            

我们还需要通过此行在 client.js 文件中启用安全套接字连接 var conn = new的WebSocket( 'WSS://本地主机: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 + "";       };       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 + "";    //sending a message to a connected peer    dataChannel.send(val);    msgInput.value = ""; });

现在通过节点服务器运行我们的安全信令服务器.在修改后的聊天演示文件夹中运行 node static .在两个浏览器选项卡中打开 localhost:8080 .尝试登录.请记住只允许使用"password1"的"user1"和"password2"的"user2"登录.然后建立RTCPeerConnection(调用另一个用户)并尝试发送消息.

建立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";  //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应用程序的范围内使用它们.